Claude Skill

playwright

Operate Playwright for browser automation end to end: author and debug E2E test suites (robust locators, network interception and mocking, parallel workers, accessibility snapshot checks), wire them into CI, and drive headless browsing and scraping with an extract -> validate ->

LLM Mart · 0 points · 10 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download magnus919-agent-skills-playwright-addad86.zip · 34 KB
Part of magnus919/agent-skills — 145 skills

Install

skills CLI npx skills add https://github.com/magnus919/agent-skills/tree/main/playwright
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install magnus919-agent-skills@llmmart
Git git clone https://github.com/magnus919/agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole magnus919/agent-skills collection as a plugin from our marketplace. Git is the plain clone.

README

Playwright — E2E Testing, Scraping, and Headless Browsing

Drive a real browser with Playwright: author and debug E2E test suites, mock network traffic, run tests in parallel and in CI, and scrape JavaScript-rendered pages — all with a bundled smoke harness that works without a browser installed.

Why Install This Skill

Your agent can operate a Playwright test suite end to end: read the config and understand what runs, write stable specs with user-facing locators, intercept and mock third-party APIs so tests stop being flaky, tune workers and sharding, and wire the suite into CI with traces and reports you can actually triage. It also covers the scraping side — loading JavaScript-rendered pages and extracting structured data with an explicit extract → validate → save loop that respects robots.txt and rate limits.

The bundled pwrun script makes the toolchain legible without any Node setup: it checks the environment, inventories the suite, and summarizes a Playwright JSON test report into a bounded failure list — so an agent can triage a red CI run from the report artifact alone, no browser needed.

What You Get

Directory Purpose
SKILL.md Agent-facing operating loop: authoring, selectors, network mocking, parallel workers, CI, scraping, accessibility snapshots, headed debugging
references/ Eight dated references: e2e authoring, selectors, network interception/mocking, parallel/sharding, CI, scraping/headless, accessibility + debugging, source index
scripts/pwrun Smoke harness with --json: doctor (toolchain), inventory (suite shape), report (JSON-report triage), smoke (delegated run)
tests/ Deterministic tests plus a sample Playwright JSON report fixture
templates/ Copy-in test-suite scaffold: playwright.config.ts, example.spec.ts, accessibility.spec.ts
evals/evals.json Output-quality evals (schema v1, 6 cases) spanning authoring, scraping, debugging, and frontend test implementation

Quick Start

# Inspect a Playwright suite and triage its runs — no node required
bash scripts/pwrun doctor --json
bash scripts/pwrun inventory --json
bash scripts/pwrun report --report test-results/test-results.json --json

# Scaffold a new suite (copy the templates into your project)
cp templates/playwright.config.ts templates/example.spec.ts templates/accessibility.spec.ts .
npm i -D @playwright/test
npx playwright@1.62.1 install
npx playwright@1.62.1 test

The --help output documents every flag and works without Node. Set BASE_URL to override the smoke target; scripts/pwrun smoke --url http://localhost:3000 --json runs a delegated pass through npx playwright@1.62.1 test.

Triggers

Load this skill for Playwright, playwright test, E2E test authoring and debugging, flaky selectors/locators, network interception and mocking (page.route), test parallelism and sharding, running E2E tests in CI, accessibility snapshot checks (toMatchAriaSnapshot, axe scans), or scraping/headless browsing of JavaScript-rendered pages. Do not load it for QA strategy or framework selection (that's qa-methodology), frontend component design (that's frontend-engineering), or Cloudflare challenge bypass (that's flaresolverr).

Requirements

  • Playwright 1.40+ for the documented patterns (aria snapshots need 1.49+).
  • Node.js + @playwright/test to run tests or the smoke command.
  • The pwrun script runs on Python 3.8+ (stdlib only) — doctor, inventory, and report need no Node or browser.
  • A display server (or headless mode) when running browsers on a CI runner.

Skill manifest

Playwright Browser Automation

Use this skill to drive a real browser with Playwright: author end-to-end tests, keep selectors and tests robust, intercept and mock network traffic, run suites across parallel workers and in CI, scrape and extract data in headless mode, and check accessibility with snapshot scans. This is a tool skill for one named tool. Test strategy and framework selection belong to qa-methodology; frontend component and architecture design belong to frontend-engineering. This skill owns operating the Playwright tool itself.

Operating contract

  1. Read the suite before running. Inspect playwright.config.*, projects, baseURL, webServer, workers, retries, and reporters before running anything. Never assume the test command from the README.
  2. Locate by behavior, not layout. Prefer user-facing locators (getByRole, getByLabel, getByText) over CSS/XPath that encode markup. Tests coupled to user-visible behavior survive refactors; tests coupled to structure break on them.
  3. Mock at the boundary. Stub external HTTP at page.route() — never by patching in-page code. Mock the dependency under test's edges, never the code under test itself; a test that mocks what it claims to verify proves nothing.
  4. Parallelize deliberately. Playwright gives every test an isolated browser context. Tune workers and sharding to the machine and suite, and never let tests share mutable state through globals.
  5. Verify at the boundary. A green test is only as strong as its assertions. Assert on user-visible outcomes (visible text, URL, enabled/disabled state), not on implementation details.
  6. Keep evidence bounded. Capture traces, screenshots, and video on failure only; summarize JSON reports instead of dumping them; never paste full HTML dumps or session cookies into chat.

The pwrun script

scripts/pwrun is an agent-first smoke harness around a Playwright suite. doctor, inventory, and report work with no node or Playwright installed, so an agent can inspect a suite and triage a CI report anywhere.

scripts/pwrun doctor --json                     # node, @playwright/test, browsers, config availability
scripts/pwrun inventory --json                  # config, projects, spec files in the suite
scripts/pwrun report --report test-results.json --json   # summarize a Playwright JSON report
scripts/pwrun smoke --url http://localhost:3000 --json   # delegate a smoke run to npx playwright test

Exit codes: 0 ok, 1 analysis error, 2 usage error, 127 dependency (node/playwright) missing, 124 delegate timeout. --json on every command; --help works without any toolchain.

E2E test authoring

  • Structure suites with test.describe blocks, test.beforeEach setup, and per-feature fixture files. Keep specs short, focused on one user journey, and readable as prose.
  • Use Playwright's web-first assertions (expect(locator).toBeVisible(), .toHaveText(), .toHaveURL()) — they auto-retry until a timeout and are the backbone of stable E2E tests. Avoid manual page.waitForTimeout sleeps.
  • Start the app under test with webServer in the config (Playwright starts it, waits for readiness, and tears it down), or reuse an already-running instance via baseURL for smoke passes.
  • Authoring patterns, fixtures, and the page-object model: references/01-e2e-authoring.md.

Selector robustness

  • Default to role, label, text, and placeholder locators — they describe the page the way a user does. Reserve CSS for layout-adjacent needs (e.g., getByTestId for ids that exist only for tests).
  • Chain and filter locators (getByRole('row').filter({ hasText: 'Acme' })) instead of building one long brittle selector. Re-query rather than storing stale element handles.
  • Fix flaky tests by finding which selector matched multiple or zero elements, not by adding sleeps or first(). A selector that can match the wrong thing will eventually.
  • Selector priority rules and flaky-selector repair flows: references/02-selectors.md.

Network interception and mocking

  • Intercept requests with page.route() and fulfill, abort, or continue them. Use this to mock third-party APIs, inject fixtures, emulate offline or slow networks, and block analytics/trackers that pollute tests.
  • Mock at the route level with realistic bodies and content types. Never intercept the server the test is supposed to verify, and never route over page.goto navigation the test depends on.
  • Capture requests with page.on('request')/page.on('response') to assert on what the app actually sent. Patterns and gotchas: references/03-network-interception-and-mocking.md.

Parallel workers

  • workers sets how many parallel processes run specs; fullyParallel lets every spec file run across workers. Scale to CPU count and browser memory, not to "as many as possible".
  • Shard suites across CI jobs (--shard=1/4) for large suites. Every test runs in its own browser context, so isolation is default — keep it that way: no shared globals, no shared storage state, no test-order coupling.
  • Worker counts, sharding math, and isolation traps: references/04-parallel-workers-and-sharding.md.

CI integration

  • Install browsers and OS deps on the runner (npx playwright@1.62.1 install --with-deps), pin the Playwright version, and cache ~/.cache/ms-playwright.
  • Configure webServer, retries (retry flaky tests on CI only), and trace: 'on-first-retry' so failures are debuggable. Report with html/json/github and upload artifacts on failure.
  • Triage CI failures from the JSON report with scripts/pwrun report --json — it summarizes stats, failing specs, and error messages without opening a browser. Full CI recipes: references/05-ci-integration.md.

Scraping and headless browsing

  • Drive the browser API directly (chromium.launch({ headless: true }), newContext, page.goto) to load JavaScript-rendered pages, then extract with locators and innerText/attribute access into structured records — extract, validate, save.
  • Respect robots.txt, terms, and rate limits; bound the scrape by page count and delay. For Cloudflare/DDoS-GUARD challenge bypass, route to flaresolverr — this skill does not solve challenges.
  • Scraping flows, pagination, and polite extraction: references/06-scraping-and-headless.md.

Accessibility snapshot checks

  • Run full scans with @axe-core/playwright to catch WCAG violations (contrast, landmarks, ARIA misuse) in CI or on demand.
  • Use Playwright's aria snapshots (expect(page).toMatchAriaSnapshot()) as stable, accessibility-aware assertions: they compare the accessibility tree, so they catch structure and label regressions and read like spec assertions. Update deliberately, never --update-snapshots reflexively.
  • Scan setup, snapshot discipline, and fixing violations: references/07-accessibility-and-debugging.md.

Headed debugging

  • Run headed (--headed), slow the action with --slow-mo, or drop into the inspector with --debug / PWDEBUG=1 and the page.pause() breakpoint.
  • Generate starter tests with npx playwright@1.62.1 codegen <url>, then harden the generated selectors into user-facing locators.
  • When a test fails: read the trace (--trace on), which records network, DOM snapshots, and console for the failed action. Use scripts/pwrun report --report <json> --json first to see the failure summary.
  • Debugging workflows live in references/07-accessibility-and-debugging.md.

Reference routing

Load when Reference
Writing or structuring specs, fixtures, page objects references/01-e2e-authoring.md
A selector is flaky or matches the wrong element references/02-selectors.md
Mocking or intercepting API/network traffic references/03-network-interception-and-mocking.md
Speeding up or sharding a large suite references/04-parallel-workers-and-sharding.md
Wiring Playwright into CI or triaging CI failures references/05-ci-integration.md
Extracting data or browsing in headless mode references/06-scraping-and-headless.md
Accessibility scans, aria snapshots, or debugging a failing test references/07-accessibility-and-debugging.md
Sources, version observations, and refresh procedure references/00-source-index.md

Included artifacts

  • scripts/pwrun: smoke harness — doctor, inventory, report, smoke, all with --json.
  • tests/test_pwrun.py + tests/fixtures/sample-report.json: deterministic tests for the harness (no node/browser required).
  • templates/playwright.config.ts, templates/example.spec.ts, templates/accessibility.spec.ts: copy-in test-suite scaffold.
  • references/: eight dated, source-indexed references covering the operational topics above.

Verification boundary

Claim Minimum evidence
Toolchain is present scripts/pwrun doctor --json reports node, @playwright/test, and browsers available
Suite is understood scripts/pwrun inventory --json lists config and spec files
A test passes npx playwright@1.62.1 test exit 0 on the targeted spec (or smoke delegation)
A CI failure is explained scripts/pwrun report --report test-results.json --json names the failing specs and errors
Accessibility is covered An axe scan runs with zero violations of the declared severity, and aria snapshots match
No regressions in the covered flows The suite ran under the configured workers/sharding with expected/flaky/unexpected counts recorded

Hard boundaries

  • Never mock the code under test to force a green test — mock only its boundaries.
  • Never scrape in violation of robots.txt, terms, or rate limits; never harvest credentials or personal data, and never extract auth/session storage into committed files.
  • Never commit playwright/.auth/* storage state, .env, or browser credentials.
  • Never run --update-snapshots blindly to "fix" an aria snapshot diff — inspect what changed first.
  • Never dump raw HTML, full trace files, or screenshots of protected content into chat; summarize with pwrun report instead.
  • A headed browser in CI needs a display server (e.g., xvfb) — never assume a display exists on a runner.

When not to use

  • Test strategy, framework selection, or QA process — route to qa-methodology.
  • Frontend component/state/architecture design or implementation guidance — route to frontend-engineering; React-specific component and hooks work routes to react.
  • Cloudflare/DDoS-GUARD challenge bypass — route to flaresolverr.
  • Load/performance testing at scale (k6, Locust, Gatling, JMeter) — that methodology lives under qa-methodology's performance-testing reference.
  • Raw HTTP retrieval of static content — use a plain HTTP client; Playwright is for JavaScript-rendered pages and browser workflows.

Topic coverage keywords

The catalog's automated topic sweep greps this file for coverage keywords. Two alias pairs are written literally below so the sweep matches both spellings: e2e|end-to-end covers browser-level tests (E2E, end-to-end, and e2e are the same workflow), and debug|head covers the headed-debugging workflows in this skill. All other topics (selector robustness, network interception, parallel workers, CI integration, scraping, accessibility snapshots, mocking) appear by name in the sections above.

Files (agent-skills)
  • evals
    • evals.json 8.8 KB
      {
        "schema_version": 1,
        "skill_name": "playwright",
        "evals": [
          {
            "id": "e2e-checkout-authoring",
            "prompt": "Write a Playwright E2E test for the checkout flow: add an item to the cart, apply a promo code, and complete the purchase. The app runs at http://localhost:3000 via npm run dev. What should the spec look like and what config is needed so it runs reliably in CI too?",
            "expected_output": "A spec structure that describes one user journey per test using test.describe and test.beforeEach, drives the flow with user-facing locators (getByRole for buttons like 'Checkout' and 'Place order', getByLabel for the promo input), and asserts with web-first assertions (toHaveText on the confirmation heading, toHaveURL on the order route) instead of waitForTimeout sleeps. The answer recommends a playwright.config.ts with testDir, baseURL, projects (desktop and mobile), webServer pointing at 'npm run dev' with a readiness URL and reuseExistingServer false on CI, retries on CI only, and trace on-first-retry, plus reporters including JSON so the run is triageable with pwrun report.",
            "assertions": [
              "The spec is structured as one user journey per test with describe/beforeEach",
              "Interactions use role and label locators rather than brittle CSS",
              "Assertions are web-first (toHaveText, toHaveURL) with no fixed sleeps",
              "The config declares webServer, baseURL, projects, CI-only retries, and trace on-first-retry",
              "The JSON reporter is included so CI failures can be triaged from the report"
            ]
          },
          {
            "id": "flaky-selector-repair",
            "prompt": "Our checkout test is flaky: it passes locally but fails in CI about 30% of the time at page.getByText('Buy now').click(), complaining the locator resolved to 0 elements or sometimes to 2. How should I investigate and fix it?",
            "expected_output": "A selector repair procedure: measure the flake deterministically by running the spec alone with --workers=1 --repeat-each=5, then use --debug or the trace to see what the locator actually resolved to. The answer explains that getByText('Buy now') is ambiguous (two elements, e.g. a button and a promo snippet) and late-rendering (0 elements because the product list loads after navigation). The fix is a role-based locator scoped to the product card — getByRole('button', { name: 'Buy now' }) inside a card filtered by product name — plus a web-first assertion on the card container before acting, and never adding waitForTimeout or first() as a band-aid. Verification is five consecutive green repeat-each runs and a green CI run.",
            "assertions": [
              "The flake is measured deterministically with --workers=1 --repeat-each before changing code",
              "The cause is diagnosed as ambiguous text match plus late rendering",
              "The fix uses a role-based locator scoped by product card, not first() or sleeps",
              "A web-first assertion on the container precedes the action",
              "Verification is repeated green runs and a green CI run"
            ]
          },
          {
            "id": "network-mock-payment-api",
            "prompt": "Our E2E suite depends on a third-party payment tokenization API that is rate-limited and sometimes unavailable, making tests flaky. How do I make the payment flow tests hermetic while still testing the app's real behavior?",
            "expected_output": "A network interception plan using page.route in test.beforeEach: fulfill the tokenization endpoint with a realistic JSON body and content type so the app receives a token as in production, register routes before navigation, and abort analytics/tracker traffic that pollutes tests. The answer warns never to mock the app's own server or the code under test, keeps the mock payloads schema-accurate, and shows asserting on the request the app actually sent via page.on('request') to verify the body. It notes that only third-party boundaries are mocked and that a controlled real-API variant is preferable for integration verification.",
            "assertions": [
              "page.route fulfills the third-party endpoint with a realistic body and content type before navigation",
              "Analytics and tracker traffic is blocked so tests stay hermetic",
              "The app's own server and the code under test are explicitly not mocked",
              "The request the app sent is asserted via page.on('request')",
              "A controlled real-API variant is offered as the integration verification option"
            ]
          },
          {
            "id": "scrape-product-catalog",
            "prompt": "A documentation site renders its product catalog table only after JavaScript loads. Scrape the table (columns: name, version, license) into a structured file without breaking the site's rules.",
            "expected_output": "A headless scraping plan following extract -> validate -> save: launch Chromium headless with an identifying user agent, load the page with wait_until networkidle, scope a locator to the repeating table rows and read cell texts into plain records, validate that every record has name/version/license before saving, and write one bounded JSON artifact with timestamp and source URL. The answer checks robots.txt and terms, adds a delay between pages, caps the scrape with MAX_RECORDS/MAX_PAGES, and refuses to extract personal data or persist auth storage. It routes Cloudflare challenge cases to flaresolverr instead of this skill.",
            "assertions": [
              "The page is loaded in a headless browser because the table is JavaScript-rendered",
              "Extraction is scoped to the repeating rows and produces structured records, not HTML blobs",
              "Records are validated for required fields before saving",
              "Robots.txt, terms, rate limits, and bounded extraction are respected",
              "Auth storage is never persisted and challenge pages route to flaresolverr"
            ]
          },
          {
            "id": "ci-failure-triage",
            "prompt": "The CI run for the E2E suite failed. The only artifact is test-results/test-results.json from the Playwright JSON reporter. The console shows '3 expected, 2 unexpected'. What are the next steps to diagnose and fix?",
            "expected_output": "A triage procedure that starts by summarizing the JSON report with the pwrun script (scripts/pwrun report --report test-results.json --json) to get the failing spec titles and the error message from the last retry without opening a browser, then opens the trace artifact for the failed tests to see the failing action, network, and console. The answer classifies the failure: environment (missing browser deps on the runner, webServer readiness), selector problem (referencing the selectors reference), or a real app regression. It prescribes fixing and re-running, and adding the trace-on-first-retry config plus artifact upload if the run lacked them, keeping evidence bounded by not dumping the full report.",
            "assertions": [
              "The JSON report is summarized with the pwrun report command before any other debugging",
              "The trace artifact is opened to inspect the failing action, network, and console",
              "The failure is classified as environment, selector, or app regression",
              "Missing trace/artifact config is added if the run lacked them",
              "Evidence stays bounded: summaries and targeted artifacts, not full dumps"
            ]
          },
          {
            "id": "frontend-test-implementation",
            "prompt": "A React team is adding a new feature (filter + sortable product list) and wants tests that prevent regressions. They ask how to implement testing for it across levels, including browser-level coverage. What should the plan look like and when is Playwright the right tool?",
            "expected_output": "A test implementation plan across the pyramid: component tests with Testing Library for state and rendering, then Playwright E2E specs for the user journeys that matter (sorting, filtering, empty state) as the browser-level layer, keeping the component-to-E2E split based on what each level proves. The answer routes the frontend implementation guidance to frontend-engineering and notes this skill owns writing and running the Playwright specs: role-based locators, web-first assertions, mocking the products API at the route boundary, parallel workers for speed, and CI wiring with webServer, retries, and the JSON reporter for triage. It flags not to E2E-test everything — component tests cover most logic, E2E covers the journeys.",
            "assertions": [
              "The plan spans component tests (Testing Library) and Playwright E2E for user journeys",
              "The split is justified by what each level proves, not by convention",
              "Playwright specs use role-based locators, web-first assertions, and route-level API mocking",
              "CI wiring (webServer, retries, JSON reporter) is included",
              "Component-versus-E2E routing guidance points to frontend-engineering for design and this skill for browser execution"
            ]
          }
        ]
      }
      
  • references
    • 00-source-index.md 2.2 KB
      # Source Index — Playwright references
      
      > **Last Updated:** 2026-08-03
      
      ## Scope
      
      This skill's references are distilled from the official Playwright documentation
      and the underlying standards the tool implements (WebDriver BiDi for CDP
      migration, WAI-ARIA for the accessibility tree). They are patterns and
      decision guidance, not a substitute for the primary sources below.
      
      ## Primary sources
      
      | Topic | Source | Accessed |
      |---|---|---|
      | Test runner, assertions, fixtures, webServer | https://playwright.dev/docs/writing-tests | 2026-08-03 |
      | Locators and selector philosophy | https://playwright.dev/docs/locators | 2026-08-03 |
      | Network interception (`page.route`) | https://playwright.dev/docs/network | 2026-08-03 |
      | Parallelism and sharding | https://playwright.dev/docs/test-parallel | 2026-08-03 |
      | CI guides (GitHub Actions, Docker) | https://playwright.dev/docs/ci | 2026-08-03 |
      | Traces, HTML/JSON reporters, debugging | https://playwright.dev/docs/trace-viewer | 2026-08-03 |
      | Accessibility testing + aria snapshots | https://playwright.dev/docs/accessibility-testing | 2026-08-03 |
      | Headless browsers and scraping contexts | https://playwright.dev/docs/api/class-browser | 2026-08-03 |
      | Codegen, inspector, slow-mo | https://playwright.dev/docs/codegen | 2026-08-03 |
      | `@axe-core/playwright` integration | https://github.com/dequelabs/axe-core-npm | 2026-08-03 |
      
      ## Version observations
      
      - **1.40+** — `toMatchAriaSnapshot` is available from 1.49; earlier versions
        used the experimental `page.accessibility` API or `expect(...).toHaveAccessibleSnapshot()`.
        Pin the version that matches the features this skill references (see `SKILL.md`).
      - The JSON reporter output shape changed over time (suite-level `tests` arrays
        became `specs[]` with nested `tests[]`). `scripts/pwrun report` parses both
        shapes; a fixture for the modern shape lives in `tests/fixtures/sample-report.json`.
      
      ## Refresh procedure
      
      Re-verify statements in these references when a new Playwright major lands:
      1. Re-read the primary source pages above.
      2. Check the changelog for renamed APIs (e.g., `--update-snapshots` flags,
         reporter options, `webServer` fields).
      3. Update the `Last Updated` line in the changed reference and the version
         observations above.
      
    • 01-e2e-authoring.md 3.5 KB
      # E2E Test Authoring
      
      > **Last Updated:** 2026-08-03
      
      How to write Playwright E2E tests that are readable, stable, and fast to
      maintain. Authoring decisions are execution-side; *what to test and at what
      level* is QA strategy owned by `qa-methodology` (its
      `qa-methodology/references/test-strategy.md`).
      
      ## Anatomy of a spec
      
      One spec file per user journey, described in prose, located by behavior:
      
      ```ts
      import { test, expect } from '@playwright/test';
      
      test.describe('checkout', () => {
        test.beforeEach(async ({ page }) => {
          await page.goto('/cart');
        });
      
        test('completes a purchase with a saved card', async ({ page }) => {
          await page.getByRole('button', { name: 'Checkout' }).click();
          await page.getByRole('button', { name: 'Place order' }).click();
          await expect(page.getByRole('heading', { level: 1 })).toHaveText('Order confirmed');
        });
      });
      ```
      
      Rules of thumb:
      
      - **One journey per test.** A test that verifies two unrelated flows fails for
        two unrelated reasons and gets rewritten as two tests anyway.
      - **Setup in `beforeEach` / fixtures, not in the test body.** Keep the test body
        readable as a spec of the behavior.
      - **Describe what the user does**, not what the DOM does: "places an order",
        not "clicks the button with class `.btn-primary`".
      
      ## Web-first assertions (no sleeps)
      
      Playwright assertions retry until a timeout:
      
      ```ts
      await expect(page.getByText('Saved')).toBeVisible();
      await expect(input).toHaveValue('50');
      await expect(page).toHaveURL(/\/orders\/\d+/);
      ```
      
      - Never `await page.waitForTimeout(2000)` to "fix" a race — it slows the suite
        and hides the real timing bug.
      - For genuinely async conditions use `expect.poll()` or `expect(...).toPass()`
        instead of arbitrary sleeps.
      
      ## Fixtures
      
      Shared setup lives in a fixture file and is composed per test:
      
      ```ts
      import { test as base, expect } from '@playwright/test';
      
      export const test = base.extend<{ signedInPage: Page }>({
        signedInPage: async ({ page }, use) => {
          await page.goto('/login');
          await page.getByLabel('Email').fill('qa@example.com');
          await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
          await page.getByRole('button', { name: 'Sign in' }).click();
          await use(page);
        },
      });
      ```
      
      ## Starting the app: webServer
      
      Declare the app lifecycle in the config so Playwright starts, waits for, and
      tears down the server per run (see `templates/playwright.config.ts`):
      
      - `webServer.command` — the dev/preview command.
      - `webServer.url` — a readiness URL; Playwright polls it before running tests.
      - `reuseExistingServer: !process.env.CI` — reuse a dev server locally, always
        start fresh on CI.
      
      Prefer `webServer` over asking the agent to start the app manually; the config
      makes the run reproducible in CI too.
      
      ## The page-object model (at the size where it pays)
      
      Group locators and actions for a screen into a class when a spec grows beyond
      ~15 lines or the same flow is asserted from several specs:
      
      ```ts
      export class CartPage {
        constructor(private readonly page: Page) {}
        async open() { await this.page.goto('/cart'); }
        async applyPromo(code: string) { await this.page.getByLabel('Promo code').fill(code); }
        get checkoutButton() { return this.page.getByRole('button', { name: 'Checkout' }); }
      }
      ```
      
      Do not add a POM layer preemptively — one spec that reads as prose beats a
      POM with one user.
      
      ## Related
      
      - Locator choice and flaky-selector repair: `02-selectors.md`.
      - Mocking external HTTP so tests stay hermetic: `03-network-interception-and-mocking.md`.
      - Snapshot-style accessibility assertions: `07-accessibility-and-debugging.md`.
      
    • 02-selectors.md 3.2 KB
      # Selector Robustness
      
      > **Last Updated:** 2026-08-03
      
      Selectors are the #1 source of E2E flakiness. The goal is locators that
      describe what the element *is* (its role in the user experience), not where it
      *happens to be* in the DOM.
      
      ## Locator priority
      
      Use, in order of preference:
      
      1. **Role** — `page.getByRole('button', { name: 'Save' })`. Mirrors how the
         page is presented to assistive tech and users; survives markup changes.
      2. **Label / placeholder / text** — `getByLabel('Email')`,
         `getByPlaceholder('Search')`, `getByText('Saved', { exact: true })`.
      3. **Test id** — `getByTestId('checkout-form')`. For elements whose role/label
         does not describe them (e.g., a decorative SVG, a canvas region). Test ids
         exist only for tests; agree on a naming convention.
      4. **CSS / XPath** — last resort: layout-adjacent queries that role and label
         cannot express (e.g., "the third row of a table" is better done with
         `getByRole('row').nth(2)`).
      
      ## Composition over long strings
      
      Chain and filter instead of concatenating brittle paths:
      
      ```ts
      // Fragile: encodes nesting and order.
      page.locator('div.product-card div.price span').click();
      
      // Robust: describe the card by its visible content, then act within it.
      const card = page.getByRole('article').filter({ hasText: 'Running shoes' });
      await card.getByRole('button', { name: 'Add to cart' }).click();
      ```
      
      - `filter({ hasText })` / `filter({ has: locator })` narrow a collection.
      - `first()`, `last()`, `nth(n)` are code smells unless the ordering is the
        assertion (e.g., a sort test).
      
      ## The repair loop
      
      A flaky test is a bug report about your selectors, not a request for more
      `waitForTimeout`. When a test passes sometimes:
      
      1. Run the spec alone (`npx playwright@1.62.1 test <spec> --workers=1 --repeat-each=5`)
         to measure flakiness deterministically.
      2. Use `--debug` or the trace to see what the failing action actually resolved.
         Common causes:
         - **Zero matches** — the element appears late (async render): use a
           web-first assertion or wait for its container, not a sleep.
         - **Multiple matches** — your locator is too generic: narrow with
           `filter({ hasText })` or scope to a container.
         - **Stale node** — the element is re-rendered between lookup and action:
           re-query instead of storing a handle, or assert the new state after the
           re-render.
      3. Fix the locator to describe the unique user-facing element, then re-run the
         repeat-each loop until it is green 5/5.
      
      ## Anti-patterns to avoid
      
      - **`page.waitForTimeout()`** — masks races, slows the suite.
      - **`page.waitForSelector()` + manual `click()`** — duplicates what
        `locator.click()` already does with retries.
      - **Snapshots of CSS classes** (`expect(el).toHaveClass(...)`) for behavioral
        assertions — classes are implementation details.
      - **Text that duplicates** across the page without disambiguation
        (`getByText('Submit')` matches 2 buttons) — add `{ exact: true }` or scope.
      - **XPath with positional predicates** (`//div[2]/span[1]`) — order and
        structure change; roles do not.
      
      ## Related
      
      - Authoring structure and fixtures: `01-e2e-authoring.md`.
      - Emulating slow networks to surface timing bugs: `03-network-interception-and-mocking.md`.
      
    • 03-network-interception-and-mocking.md 3.1 KB
      # Network Interception and Mocking
      
      > **Last Updated:** 2026-08-03
      
      Intercept browser traffic with `page.route()` to make E2E tests hermetic,
      deterministic, and fast — and to assert on what the app actually sent.
      
      ## The route API
      
      ```ts
      // Fulfill: answer an intercepted request with canned data.
      await page.route('**/api/payments/tokenize', (route) =>
        route.fulfill({
          status: 200,
          contentType: 'application/json',
          body: JSON.stringify({ token: 'tok_test_123' }),
        }),
      );
      
      // Abort: block a request entirely (trackers, analytics, heavy media).
      await page.route('**/analytics/*', (route) => route.abort());
      
      // Continue: let the real request through (with optional overrides).
      await page.route('**/api/feature-flags', (route) =>
        route.continue({ headers: { ...route.request().headers(), 'x-experiment': 'on' } }),
      );
      ```
      
      Guidance:
      
      - **Mock at the boundary.** The app under test is real; the world outside it
        (payment providers, third-party APIs, message queues) is faked. Never patch
        in-page code (`window.fetch = ...`) — that tests a tampered app.
      - **Realistic bodies win.** Mock payloads that match the real schema and
        content type; a mock that differs from production can pass a test the app
        would fail.
      - **Glob `**` patterns, not exact URLs** — hosts, query strings, and CDN
        prefixes change; `**/api/orders` covers `https://api.example.com/api/orders`.
      - **Register routes before navigation** — routes apply to requests made after
        registration, so set them up in `test.beforeEach` before `page.goto`.
      
      ## What NOT to mock
      
      - The server under test: if the suite verifies the app + its backend
        integration, intercepting that API makes the test meaningless. Mock only
        *third-party* boundaries, or test the real API via a controlled
        environment.
      - `page.goto()` navigation that the test depends on — routing over the app's
        own document requests breaks the flow the test is verifying.
      - Responses whose timing you are testing (e.g., loading states): use
        `route.fulfill` with a small artificial delay or the API's
        `page.clock`/routing delay, never a fixed `waitForTimeout`.
      
      ## Asserting on traffic
      
      Capture what the app sent, then assert on it:
      
      ```ts
      let tokenizeBody: string | undefined;
      page.on('request', (request) => {
        if (request.url().includes('/api/payments/tokenize')) tokenizeBody = request.postData();
      });
      // ... drive the flow ...
      expect(JSON.parse(tokenizeBody!)).toMatchObject({ amount: 4990, currency: 'usd' });
      ```
      
      Use `page.on('response')` to wait for a specific status instead of guessing
      when the network settled:
      
      ```ts
      await page.waitForResponse((response) => response.url().includes('/api/orders') && response.status() === 201);
      ```
      
      ## Emulating network conditions
      
      ```ts
      await page.route('**/*', (route) =>
        route.continue().catch(() => {})
      );
      await page.context().setOffline(true); // test offline/error states
      // or throttle via context options: offline, latency, downloadThroughput...
      ```
      
      ## Related
      
      - Locators that survive the mocked world: `02-selectors.md`.
      - Running these tests in parallel without interference: `04-parallel-workers-and-sharding.md`.
      
    • 04-parallel-workers-and-sharding.md 2.7 KB
      # Parallel Workers and Sharding
      
      > **Last Updated:** 2026-08-03
      
      Playwright runs each test in its **own browser context** — isolation is the
      default. Parallelism is about scaling that safely, and sharding is how large
      suites split across CI machines.
      
      ## Workers
      
      - `workers` in the config caps how many parallel worker *processes* run at once.
        Each worker hosts one browser instance and runs one test at a time.
      - `fullyParallel: true` lets every spec file run across workers; with it off,
        only files in separate *projects* run in parallel.
      - **Size workers to the machine**, not to desire: each Chromium worker needs
        roughly 300–500 MB. A 2-core/4 GB runner with 4 Chromium workers will OOM.
        Start at `Math.min(cores, 4)` and measure.
      - CI: pin `workers: 4` (or `--workers=4`) for a stable runtime; `undefined`
        locally lets Playwright pick.
      
      ```ts
      workers: process.env.CI ? 4 : undefined,
      fullyParallel: true,
      ```
      
      ## Sharding
      
      Split one suite across multiple CI jobs:
      
      ```bash
      npx playwright test --shard=1/4   # job 1 of 4
      ```
      
      Each shard runs a disjoint set of tests; the HTML report aggregates via
      `merge-reports`. Shard count should roughly equal runner count; the sharded
      runtime is the slowest shard, so balance by spec-file count, not total tests
      (Playwright shards by file).
      
      ## Isolation traps (things that break parallel runs)
      
      - **Shared global state in the app under test** — a localStorage flag, a
        singleton cache, a shared DB row: workers race and tests interfere. Reset
        per test (fixtures that clean up, `test.beforeEach` seeding).
      - **Shared files on disk** — screenshots/traces written to the same path from
        two workers. Give each test its own output dir (`test-results/<project>/` is
        the default; don't override to one shared file).
      - **Port collisions** — multiple webServers or `page.goto('http://localhost:3000')`
        hardcoded across workers. Use `webServer` with one process, or unique ports
        per project.
      - **Test-order dependence** — `describe.only`/`test.skip` patterns, tests that
        assume a previous test ran. Every test must pass alone (`--grep` it) and in
        any order (`--workers=1 --repeat-each=3` to check determinism).
      
      ## Verifying parallelism is working
      
      ```bash
      scripts/pwrun inventory --json        # suite shape
      npx playwright test --list            # what will run
      npx playwright test --workers=4       # parallel run
      ```
      
      Compare `--workers=1` vs `--workers=4` wall time: healthy suites scale ~linearly
      until CPU-bound. If the parallel run is *slower* or flakier than serial, you
      have an isolation trap — see above.
      
      ## Related
      
      - CI job wiring for shards and artifacts: `05-ci-integration.md`.
      - Hermetic mocking so workers don't depend on live external APIs:
        `03-network-interception-and-mocking.md`.
      
    • 05-ci-integration.md 3.3 KB
      # CI Integration
      
      > **Last Updated:** 2026-08-03
      
      Playwright in CI is: install browsers + OS deps, pin the version, run the
      suite with retries and tracing, and surface a debuggable report. This
      reference assumes GitHub Actions; the same shape applies to any runner.
      
      ## Minimal GitHub Actions workflow
      
      ```yaml
      name: e2e
      on: [push, pull_request]
      jobs:
        e2e:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with: { node-version: 20, cache: npm }
            - run: npm ci
            - run: npx playwright@1.62.1 install --with-deps chromium
            - run: npx playwright@1.62.1 test
            - if: failure()
              uses: actions/upload-artifact@v4
              with:
                name: playwright-report
                path: playwright-report/
                retention-days: 14
      ```
      
      ## Non-negotiables
      
      1. **Install browsers with OS deps**: `npx playwright@1.62.1 install --with-deps` (not
         bare `install`) on Linux runners; `--with-deps` installs the system
         libraries Chromium/Firefox/WebKit need.
      2. **Pin and cache**:
         - `npm ci` with a committed `package-lock.json` (never `npm install`).
         - Cache the browser download: `~/.cache/ms-playwright` (Linux),
           `~/Library/Caches/ms-playwright` (macOS), `%USERPROFILE%\AppData\Local\ms-playwright` (Windows).
         - Cache `node_modules` via the setup-node `cache: npm` option.
      3. **Retry flaky tests on CI only**, with traces on retry so failures are
         debuggable:
      
         ```ts
         retries: process.env.CI ? 2 : 0,
         use: { trace: 'on-first-retry' },
         ```
      4. **Configure `webServer` in the config** so the runner starts and waits for
         the app; never assume a long-lived dev server on a runner.
      5. **Upload artifacts on failure**: the HTML report, the JSON report, and the
         `test-results/` dir (traces). Retention bounded (7–14 days) — see hard
         boundaries in `SKILL.md` about keeping evidence bounded.
      
      ## Reporters
      
      - `list`/`line` — human-readable run output.
      - `html` — the browsable report (upload on failure).
      - `json` — the machine-readable report for agent triage:
      
        ```bash
        scripts/pwrun report --report test-results/test-results.json --json
        ```
      
        It prints stats (expected/unexpected/flaky/skipped), the failing specs, and
        the error message from the last retry — enough to triage without opening a
        browser.
      - `github` — inline annotations on GitHub Actions, keyed to the failing spec
        line.
      
      ## Sharding across jobs
      
      For large suites, split the run:
      
      ```yaml
      strategy:
        matrix:
          shard: [1/4, 2/4, 3/4, 4/4]
      steps:
        - run: npx playwright@1.62.1 test --shard=${{ matrix.shard }}
      ```
      
      Merge reports from all shards with `playwright merge-reports` (see
      `04-parallel-workers-and-sharding.md` for the math).
      
      ## Triage loop for a red CI run
      
      1. `scripts/pwrun report --report <json> --json` — get the failing specs and
         messages.
      2. Download the trace artifact (`trace.zip`) and open it in the Trace Viewer
         to see the failing action, network, and console.
      3. Classify: environment (missing dep/browser), selector (see
         `02-selectors.md`), timing (webServer readiness, `webServer.timeout`),
         or app regression (real bug — the test did its job).
      4. Fix, re-run, and confirm the shard matrix is green.
      
      ## Related
      
      - Parallelism and sharding configuration: `04-parallel-workers-and-sharding.md`.
      - Trace reading and headed debugging: `07-accessibility-and-debugging.md`.
      
    • 06-scraping-and-headless.md 3.4 KB
      # Scraping and Headless Browsing
      
      > **Last Updated:** 2026-08-03
      
      Use Playwright as a headless browser for pages that need JavaScript to render:
      load the page, extract structured records, validate them, and save — the
      extract → validate → save loop. This is browser *automation*, not a generic
      HTTP client; for static content prefer a plain HTTP request (see
      `SKILL.md` → When not to use).
      
      ## The loop
      
      ```python
      # Rough shape — the same pattern in JS: launch, context, page, extract, validate, save.
      # (Playwright's Python package mirrors the Node API 1:1.)
      from playwright.sync_api import sync_playwright
      
      with sync_playwright() as p:
          browser = p.chromium.launch(headless=True)
          page = browser.new_context(
              user_agent="research-bot/1.0 (+contact@example.com)",
              viewport={"width": 1280, "height": 800},
          ).new_page()
          page.goto("https://docs.example.com/catalog", wait_until="networkidle")
          records = []
          for row in page.locator("table tbody tr").all():
              cells = row.locator("td").all_inner_texts()
              records.append(dict(zip(["name", "version", "license"], cells)))
          # validate: every record has the required fields
          valid = [r for r in records if r["name"] and r["version"]]
          # save: write one JSON/CSV artifact, bounded
          ...
      ```
      
      ## Extract → validate → save, explicitly
      
      1. **Extract** — scope locators to the repeating container; use
         `.all_inner_texts()` / attribute reads to build plain records. Never return
         raw HTML blobs.
      2. **Validate** — check required fields, types, and invariants *before* saving;
         drop or flag records that fail. A scrape that saves garbage is worse than
         one that reports failure.
      3. **Save** — write one bounded artifact (JSON/CSV) per scrape with the
         timestamp and source URL in the artifact. Do not dump page HTML or screenshots
         of protected content into chat (hard boundaries in `SKILL.md`).
      
      ## Pagination and infinite scroll
      
      - **Pagination**: follow the "next" button until it is disabled or the count
        target is reached:
      
        ```ts
        while (await nextButton.isEnabled() && records.length < MAX) {
          // extract current page...
          await nextButton.click();
          await expect(page.locator('tbody tr').first()).toBeVisible();
        }
        ```
      - **Infinite scroll**: scroll to the bottom and wait for the container's
        height/record count to grow, with a max-iteration guard.
      - Always cap: `MAX_RECORDS` and `MAX_PAGES` with a clear stop message when hit.
      
      ## Politeness and legality (hard constraints)
      
      - Respect `robots.txt`, the site's terms of service, and rate limits. Playwright
        does not enforce them; the operator does.
      - Add a delay between page loads (e.g., 1–3 s) and keep concurrency low; a burst
        of headless browsers is indistinguishable from an attack.
      - Set an identifying `user_agent` with a contact address.
      - **No credential harvesting, no personal data extraction, no auth-session
        persistence into committed files.** Storage state (`context.storage_state()`)
        must never be committed (see hard boundaries).
      - **Challenge pages**: Cloudflare/DDoS-GUARD challenges are out of scope here —
        route to [flaresolverr](../flaresolverr/SKILL.md).
      
      ## Related
      
      - Locating repeating elements robustly: `02-selectors.md`.
      - Blocking analytics/trackers while scraping: `03-network-interception-and-mocking.md`.
      - Debugging a scrape that misses content (headed, slow-mo, trace):
        `07-accessibility-and-debugging.md`.
      
    • 07-accessibility-and-debugging.md 3.5 KB
      # Accessibility and Debugging
      
      > **Last Updated:** 2026-08-03
      
      Two workflows that share one tool feature set: asserting accessibility via the
      accessibility tree, and debugging tests with the inspector, codegen, and
      traces.
      
      ## Accessibility snapshot checks
      
      ### Full scans with axe-core
      
      `@axe-core/playwright` runs the axe engine against the rendered page:
      
      ```ts
      import AxeBuilder from '@axe-core/playwright';
      
      const results = await new AxeBuilder({ page }).analyze();
      // results.violations: [{ id, impact, nodes, ... }]
      expect(results.violations.filter((v) => v.impact === 'critical' || v.impact === 'serious'))
        .toEqual([]);
      ```
      
      - Scan every route that matters, ideally in CI; scan the full page or use
        `.include()` / `.exclude()` to scope.
      - Triage by `impact`: fix critical/serious; track moderate/minor in a backlog.
      - False positives happen (e.g., contrast rules on known-brand colors) — scope
        them out deliberately, never with a blanket `disableRules(['color-contrast'])`.
      
      ### Aria snapshots (snapshot-based accessibility assertions)
      
      `expect(page).toMatchAriaSnapshot()` asserts against the **accessibility tree**,
      not the DOM:
      
      ```ts
      await expect(page).toMatchAriaSnapshot(`
        - heading "Store front" [level=1]
        - button "Add to cart"
      `);
      ```
      
      - These read like spec assertions ("the heading is X, the button is Y") and
        catch regressions in structure, labels, and semantics — not just contrast.
      - They are stable: inline text changes show up as a reviewable diff.
      - **Update deliberately.** Run `npx playwright@1.62.1 test --update-snapshots` only
        after inspecting what changed; never blind-update to make CI green (hard
        boundary in `SKILL.md`).
      - Requires Playwright 1.49+ (see `00-source-index.md` for version notes).
      
      ## Headed debugging
      
      When a test fails or a locator matches nothing:
      
      1. **Read the failure first** — `scripts/pwrun report --report <json> --json`
         gives the failing spec and the error message from the last retry.
      2. **Run headed with slow-mo** to watch the actual page:
      
         ```bash
         npx playwright@1.62.1 test <spec> --headed --slow-mo 300
         ```
      3. **The inspector** (`--debug` or `PWDEBUG=1`) pauses before each action and
         shows the current locator; `page.pause()` drops a breakpoint mid-test.
      4. **Codegen** to prototype a flow quickly:
      
         ```bash
         npx playwright@1.62.1 codegen https://example.com
         ```
         Generate starter tests, then harden the emitted selectors into user-facing
         locators (`02-selectors.md`).
      5. **Traces** are the evidence record: with `trace: 'on-first-retry'` (or
         `--trace on`), every failed run produces a trace you can open in the Trace
         Viewer — network, DOM snapshots, console, and the failing action, frame by
         frame. This is the primary artifact to attach to a bug report.
      
      ## Debugging checklist
      
      | Symptom | First move |
      |---|---|
      | Locator resolves to 0 elements | `--debug`; check async render — assert on a container first |
      | Locator matches 2+ elements | Narrow with `filter({ hasText })` or scope to a container |
      | Timeout on `click()` | Trace: is something covering the element (overlay)? is it disabled? |
      | Passes locally, fails CI | Compare env: browser deps (`install --with-deps`), `baseURL`, shard isolation |
      | Flaky across retries | `--repeat-each=5 --workers=1` to measure; then fix the selector |
      | Console errors before failure | Trace console tab; check for unhandled rejections the test should assert |
      
      ## Related
      
      - Authoring and assertions: `01-e2e-authoring.md`.
      - Selector repair loop: `02-selectors.md`.
      - CI trace/artifact wiring: `05-ci-integration.md`.
      
  • scripts
    • pwrun 15.9 KB · in bundle
  • templates
    • accessibility.spec.ts 1.2 KB
      import { test, expect } from '@playwright/test';
      import AxeBuilder from '@axe-core/playwright';
      
      /**
       * Accessibility smoke scaffold.
       *
       * Requires: npm i -D @axe-core/playwright
       *
       * - The axe scan catches WCAG violations (contrast, landmarks, ARIA misuse).
       * - The aria snapshot is a stable, accessibility-aware assertion: it compares
       *   the accessibility tree, so it catches structure and label regressions.
       *
       * When the aria snapshot legitimately changes, run
       * `npx playwright test --update-snapshots` ONLY after reviewing the diff.
       */
      test.describe('accessibility', () => {
        test('home page has no critical or serious axe violations', async ({ page }) => {
          await page.goto('/'); // [fill: route]
          const results = await new AxeBuilder({ page }).analyze();
          const blocking = results.violations.filter(
            (violation) => violation.impact === 'critical' || violation.impact === 'serious',
          );
          expect(blocking).toEqual([]);
        });
      
        test('home page matches the aria snapshot', async ({ page }) => {
          await page.goto('/');
          await expect(page).toMatchAriaSnapshot(`
            - heading "[fill: page heading]" [level=1]
            - button "[fill: primary action]"
          `);
        });
      });
      
    • example.spec.ts 948 B
      import { test, expect } from '@playwright/test';
      
      /**
       * Spec scaffold: one spec per user journey, described in prose, located by
       * user-facing roles/labels, asserted with web-first assertions. Copy into
       * e2e/ and adapt the [fill: ...] markers.
       */
      test.describe('[fill: feature under test]', () => {
        test.beforeEach(async ({ page }) => {
          await page.goto('/'); // [fill: app entry route, relative to baseURL]
        });
      
        test('[fill: the behavior in one sentence]', async ({ page }) => {
          // Prefer getByRole / getByLabel / getByText over CSS that encodes markup.
          await page.getByRole('button', { name: '[fill: button label]' }).click();
      
          // Web-first assertions auto-retry until the timeout; never use
          // page.waitForTimeout() to "fix" a race.
          await expect(
            page.getByRole('heading', { level: 1 }),
          ).toHaveText('[fill: expected heading]');
      
          await expect(page).toHaveURL(/\/[fill: path-pattern]/);
        });
      });
      
    • playwright.config.ts 1.4 KB
      import { defineConfig, devices } from '@playwright/test';
      
      /**
       * Test-suite scaffold for Playwright.
       *
       * Copy this file (plus example.spec.ts and accessibility.spec.ts) into your
       * project root, adjust the [fill: ...] markers, and run:
       *
       *   npm i -D @playwright/test
       *   npx playwright install
       *   npx playwright test
       */
      export default defineConfig({
        testDir: './e2e', // [fill: directory that holds your spec files]
        fullyParallel: true,
        forbidOnly: !!process.env.CI,
        retries: process.env.CI ? 2 : 0, // retry flaky tests on CI only
        workers: process.env.CI ? 4 : undefined, // [fill: CI worker count for your runner]
        reporter: [
          ['list'],
          ['html', { open: 'never' }],
          ['json', { outputFile: 'test-results/test-results.json' }], // triage with scripts/pwrun report
        ],
        use: {
          baseURL: process.env.BASE_URL ?? 'http://localhost:3000', // [fill: app URL]
          trace: 'on-first-retry', // capture a trace when a test fails on retry
          screenshot: 'only-on-failure',
          video: 'retain-on-failure',
        },
        projects: [
          { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
          { name: 'mobile-chromium', use: { ...devices['Pixel 7'] } }, // [fill: devices you support]
        ],
        webServer: {
          command: 'npm run dev', // [fill: your app's dev/preview command]
          url: 'http://localhost:3000', // [fill: readiness URL the server must answer]
          reuseExistingServer: !process.env.CI,
          timeout: 120_000,
        },
      });
      
  • tests
    • fixtures
      • sample-report.json 8.7 KB
        {
          "config": {
            "configFile": "playwright.config.ts",
            "rootDir": "/repo/shop-frontend",
            "forbidOnly": false,
            "fullyParallel": true,
            "globalTimeout": 0,
            "maxFailures": 0,
            "preserveOutput": "always",
            "projects": [
              {
                "outputDir": "test-results",
                "repeatEach": 1,
                "retries": 2,
                "id": "project-chromium",
                "name": "chromium",
                "testDir": "e2e",
                "testMatch": ["**/*.spec.ts"]
              },
              {
                "outputDir": "test-results",
                "repeatEach": 1,
                "retries": 2,
                "id": "project-mobile",
                "name": "mobile-chromium",
                "testDir": "e2e",
                "testMatch": ["**/*.spec.ts"]
              }
            ],
            "reporter": [["list"], ["json", { "outputFile": "test-results.json" }]],
            "reportSlowTests": { "max": 5, "threshold": 60000 },
            "shard": null,
            "updateSnapshots": "missing",
            "version": "1.49.1",
            "workers": 4,
            "webServer": {
              "command": "npm run dev",
              "url": "http://localhost:3000",
              "reuseExistingServer": true,
              "timeout": 120000
            }
          },
          "errors": [],
          "stats": {
            "startTime": "2026-08-03T09:30:00.000Z",
            "duration": 18240,
            "expected": 5,
            "unexpected": 1,
            "flaky": 0,
            "skipped": 1
          },
          "suites": [
            {
              "title": "",
              "file": "",
              "line": 0,
              "column": 0,
              "specs": [],
              "suites": [
                {
                  "title": "checkout flow",
                  "file": "/repo/shop-frontend/e2e/checkout.spec.ts",
                  "line": 1,
                  "column": 1,
                  "specs": [
                    {
                      "title": "adds an item to the cart",
                      "ok": true,
                      "tags": [],
                      "tests": [
                        {
                          "timeout": 30000,
                          "annotations": [],
                          "expectedStatus": "passed",
                          "projectId": "project-chromium",
                          "projectName": "chromium",
                          "results": [
                            {
                              "workerIndex": 0,
                              "status": "passed",
                              "duration": 812,
                              "error": null,
                              "stdout": [],
                              "stderr": [],
                              "retry": 0,
                              "startTime": "2026-08-03T09:30:01.120Z",
                              "attachments": [],
                              "steps": []
                            }
                          ],
                          "status": "expected"
                        }
                      ],
                      "id": "spec-cart-add"
                    },
                    {
                      "title": "completes the purchase with a saved card",
                      "ok": false,
                      "tags": [],
                      "tests": [
                        {
                          "timeout": 30000,
                          "annotations": [],
                          "expectedStatus": "passed",
                          "projectId": "project-chromium",
                          "projectName": "chromium",
                          "results": [
                            {
                              "workerIndex": 0,
                              "status": "failed",
                              "duration": 30000,
                              "error": {
                                "message": "Timeout 30000ms exceeded.\n=========================== logs ===========================\nwaiting for locator('button:has-text(\"Place order\")')\n  locator resolved to 0 elements\n  locator resolved to 0 elements\n  locator resolved to 0 elements"
                              },
                              "stdout": [],
                              "stderr": [],
                              "retry": 0,
                              "startTime": "2026-08-03T09:30:02.000Z",
                              "attachments": [],
                              "steps": []
                            },
                            {
                              "workerIndex": 1,
                              "status": "failed",
                              "duration": 30000,
                              "error": {
                                "message": "Timeout 30000ms exceeded.\n=========================== logs ===========================\nwaiting for locator('button:has-text(\"Place order\")')\n  locator resolved to 0 elements"
                              },
                              "stdout": [],
                              "stderr": [],
                              "retry": 1,
                              "startTime": "2026-08-03T09:30:33.000Z",
                              "attachments": [],
                              "steps": []
                            },
                            {
                              "workerIndex": 2,
                              "status": "failed",
                              "duration": 30000,
                              "error": {
                                "message": "Timeout 30000ms exceeded.\n=========================== logs ===========================\nwaiting for locator('button:has-text(\"Place order\")')\n  locator resolved to 0 elements\n  page.on('dialog') or dialogs opened by user actions"
                              },
                              "stdout": [],
                              "stderr": [],
                              "retry": 2,
                              "startTime": "2026-08-03T09:31:04.000Z",
                              "attachments": [],
                              "steps": []
                            }
                          ],
                          "status": "unexpected"
                        }
                      ],
                      "id": "spec-checkout-purchase"
                    },
                    {
                      "title": "applies a promo code",
                      "ok": true,
                      "tags": [],
                      "tests": [
                        {
                          "timeout": 30000,
                          "annotations": [],
                          "expectedStatus": "passed",
                          "projectId": "project-chromium",
                          "projectName": "chromium",
                          "results": [
                            {
                              "workerIndex": 1,
                              "status": "passed",
                              "duration": 934,
                              "error": null,
                              "stdout": [],
                              "stderr": [],
                              "retry": 0,
                              "startTime": "2026-08-03T09:30:03.000Z",
                              "attachments": [],
                              "steps": []
                            }
                          ],
                          "status": "expected"
                        }
                      ],
                      "id": "spec-promo"
                    },
                    {
                      "title": "shows an empty cart message",
                      "ok": true,
                      "tags": [],
                      "tests": [
                        {
                          "timeout": 30000,
                          "annotations": [],
                          "expectedStatus": "passed",
                          "projectId": "project-chromium",
                          "projectName": "chromium",
                          "results": [
                            {
                              "workerIndex": 0,
                              "status": "passed",
                              "duration": 421,
                              "error": null,
                              "stdout": [],
                              "stderr": [],
                              "retry": 0,
                              "startTime": "2026-08-03T09:30:04.000Z",
                              "attachments": [],
                              "steps": []
                            }
                          ],
                          "status": "expected"
                        }
                      ],
                      "id": "spec-empty-cart"
                    },
                    {
                      "title": "validates cart totals",
                      "ok": true,
                      "tags": [],
                      "tests": [
                        {
                          "timeout": 30000,
                          "annotations": [],
                          "expectedStatus": "passed",
                          "projectId": "project-mobile",
                          "projectName": "mobile-chromium",
                          "results": [
                            {
                              "workerIndex": 3,
                              "status": "passed",
                              "duration": 1102,
                              "error": null,
                              "stdout": [],
                              "stderr": [],
                              "retry": 0,
                              "startTime": "2026-08-03T09:30:05.000Z",
                              "attachments": [],
                              "steps": []
                            }
                          ],
                          "status": "expected"
                        }
                      ],
                      "id": "spec-totals"
                    },
                    {
                      "title": "runs checkout on a phone viewport",
                      "ok": false,
                      "tags": ["@mobile"],
                      "tests": [
                        {
                          "timeout": 30000,
                          "annotations": [],
                          "expectedStatus": "passed",
                          "projectId": "project-mobile",
                          "projectName": "mobile-chromium",
                          "results": [],
                          "status": "skipped"
                        }
                      ],
                      "id": "spec-checkout-mobile"
                    }
                  ],
                  "suites": []
                }
              ]
            }
          ]
        }
        
    • test_pwrun.py 6.8 KB
      #!/usr/bin/env python3
      """Deterministic tests for the playwright/scripts/pwrun harness.
      
      Runs the script as a subprocess so the tests exercise the real CLI surface
      (--help, --help --json, doctor, inventory, report, smoke). No node or browser
      is required: report analysis and inventory run on stdlib alone, and smoke
      degrades gracefully when the Node toolchain is missing.
      """
      import json
      import os
      import shutil
      import subprocess
      import sys
      import tempfile
      import unittest
      from pathlib import Path
      
      ROOT = Path(__file__).resolve().parent.parent
      SCRIPT = ROOT / "scripts" / "pwrun"
      FIXTURE = ROOT / "tests" / "fixtures" / "sample-report.json"
      
      
      def run_script(*args: str, cwd: str | None = None) -> subprocess.CompletedProcess:
          return subprocess.run(
              [sys.executable, str(SCRIPT), *args],
              capture_output=True,
              text=True,
              cwd=cwd,
              timeout=30,
          )
      
      
      class HelpTests(unittest.TestCase):
          def test_help_exits_zero_and_advertises_json(self):
              proc = run_script("--help")
              self.assertEqual(proc.returncode, 0)
              self.assertIn("--json", proc.stdout)
              for command in ("doctor", "inventory", "report", "smoke"):
                  self.assertIn(command, proc.stdout)
      
          def test_help_json_emits_parseable_json(self):
              proc = run_script("--help", "--json")
              self.assertEqual(proc.returncode, 0)
              payload = json.loads(proc.stdout)
              self.assertEqual(payload["name"], "pwrun")
              self.assertIn("--json", [flag["name"] for flag in payload["flags"]])
      
          def test_subcommand_help_exits_zero(self):
              for command in ("doctor", "inventory", "report", "smoke"):
                  proc = run_script(command, "--help")
                  self.assertEqual(proc.returncode, 0, command)
                  self.assertIn("--json", proc.stdout)
      
      
      class ReportTests(unittest.TestCase):
          def test_report_summarizes_fixture(self):
              proc = run_script("report", "--report", str(FIXTURE), "--json")
              self.assertEqual(proc.returncode, 1, proc.stderr)  # unexpected failures present
              payload = json.loads(proc.stdout)
              self.assertFalse(payload["ok"])
              self.assertEqual(payload["stats"]["expected"], 5)
              self.assertEqual(payload["stats"]["unexpected"], 1)
              self.assertEqual(payload["stats"]["skipped"], 1)
              self.assertEqual(len(payload["failures"]), 1)
              failure = payload["failures"][0]
              self.assertEqual(failure["title"], "completes the purchase with a saved card")
              self.assertIn("Place order", failure["error"])
              self.assertIn("5 expected, 1 unexpected", payload["summary"])
      
          def test_report_flag_survives_subcommand_position(self):
              # --report before the subcommand must survive argparse namespace merging.
              proc = run_script("--report", str(FIXTURE), "report", "--json")
              self.assertEqual(proc.returncode, 1, proc.stderr)
              payload = json.loads(proc.stdout)
              self.assertFalse(payload["ok"])
              self.assertEqual(payload["stats"]["unexpected"], 1)
      
          def test_report_requires_file(self):
              proc = run_script("report", "--json")
              self.assertEqual(proc.returncode, 2)
              payload = json.loads(proc.stdout)
              self.assertIn("--report", payload["error"])
      
          def test_report_rejects_non_json(self):
              with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as handle:
                  handle.write("this is not json")
                  bad_path = handle.name
              try:
                  proc = run_script("report", "--report", bad_path, "--json")
              finally:
                  os.unlink(bad_path)
              self.assertEqual(proc.returncode, 1)
              payload = json.loads(proc.stdout)  # error path still emits parseable JSON
              self.assertFalse(payload["ok"])
      
      
      class InventoryTests(unittest.TestCase):
          def test_inventory_describes_suite(self):
              with tempfile.TemporaryDirectory() as tmp:
                  (Path(tmp) / "playwright.config.ts").write_text(
                      "export default { projects: [ { name: 'chromium' }, { name: 'firefox' } ] };\n",
                      encoding="utf-8",
                  )
                  (Path(tmp) / "e2e").mkdir()
                  (Path(tmp) / "e2e" / "checkout.spec.ts").write_text("import { test } from '@playwright/test';\n", encoding="utf-8")
                  proc = run_script("inventory", "--json", cwd=tmp)
              self.assertEqual(proc.returncode, 0, proc.stderr)
              payload = json.loads(proc.stdout)
              self.assertTrue(payload["ok"])
              self.assertEqual(payload["config"], "playwright.config.ts")
              self.assertIn("chromium", payload["projects"])
              self.assertEqual(payload["spec_count"], 1)
              self.assertTrue(payload["specs"][0].endswith("e2e/checkout.spec.ts"))
      
          def test_inventory_no_config(self):
              with tempfile.TemporaryDirectory() as tmp:
                  proc = run_script("inventory", "--json", cwd=tmp)
              self.assertEqual(proc.returncode, 0)
              payload = json.loads(proc.stdout)
              self.assertIsNone(payload["config"])
              self.assertEqual(payload["spec_count"], 0)
      
      
      class DoctorTests(unittest.TestCase):
          def test_doctor_emits_json_without_toolchain(self):
              proc = run_script("doctor", "--json")
              self.assertEqual(proc.returncode, 0, proc.stderr)
              payload = json.loads(proc.stdout)
              self.assertIn("node_found", payload)
              self.assertIn("config", payload)
              self.assertIn("browsers_available", payload)
      
          def test_doctor_config_detection(self):
              with tempfile.TemporaryDirectory() as tmp:
                  (Path(tmp) / "playwright.config.js").write_text("module.exports = {};\n", encoding="utf-8")
                  proc = run_script("doctor", "--json", cwd=tmp)
              self.assertEqual(proc.returncode, 0)
              payload = json.loads(proc.stdout)
              self.assertEqual(payload["config"], "playwright.config.js")
      
      
      class SmokeTests(unittest.TestCase):
          @unittest.skipUnless(shutil.which("node") is None, "node present; missing-toolchain path not exercised")
          def test_smoke_without_node_reports_missing_dependency(self):
              proc = run_script("smoke", "--json")
              self.assertEqual(proc.returncode, 127)
              payload = json.loads(proc.stdout)
              self.assertFalse(payload["ok"])
              self.assertIn("node", payload["error"])
              # The command parses without any extra flags; defaults are applied.
              self.assertIn("url", payload)
      
          @unittest.skipIf(shutil.which("node") is None, "node absent; delegate path not exercised")
          def test_smoke_with_node_emits_json_envelope(self):
              proc = run_script("smoke", "--json")
              # With node present but no guaranteed playwright install, the delegate
              # exits 0 (pass), 1 (playwright/npx error surfaced as JSON), or 124.
              self.assertIn(proc.returncode, (0, 1, 124))
              payload = json.loads(proc.stdout)
              self.assertIn("ok", payload)
              self.assertIn("command", payload)
      
      
      if __name__ == "__main__":
          unittest.main()
      
  • README.md 3.6 KB
    # Playwright — E2E Testing, Scraping, and Headless Browsing
    
    Drive a real browser with Playwright: author and debug E2E test suites, mock network traffic, run tests in parallel and in CI, and scrape JavaScript-rendered pages — all with a bundled smoke harness that works without a browser installed.
    
    ## Why Install This Skill
    
    Your agent can operate a Playwright test suite end to end: read the config and understand what runs, write stable specs with user-facing locators, intercept and mock third-party APIs so tests stop being flaky, tune workers and sharding, and wire the suite into CI with traces and reports you can actually triage. It also covers the scraping side — loading JavaScript-rendered pages and extracting structured data with an explicit extract → validate → save loop that respects robots.txt and rate limits.
    
    The bundled `pwrun` script makes the toolchain legible without any Node setup: it checks the environment, inventories the suite, and summarizes a Playwright JSON test report into a bounded failure list — so an agent can triage a red CI run from the report artifact alone, no browser needed.
    
    ## What You Get
    
    | Directory | Purpose |
    |---|---|
    | `SKILL.md` | Agent-facing operating loop: authoring, selectors, network mocking, parallel workers, CI, scraping, accessibility snapshots, headed debugging |
    | `references/` | Eight dated references: e2e authoring, selectors, network interception/mocking, parallel/sharding, CI, scraping/headless, accessibility + debugging, source index |
    | `scripts/pwrun` | Smoke harness with `--json`: `doctor` (toolchain), `inventory` (suite shape), `report` (JSON-report triage), `smoke` (delegated run) |
    | `tests/` | Deterministic tests plus a sample Playwright JSON report fixture |
    | `templates/` | Copy-in test-suite scaffold: `playwright.config.ts`, `example.spec.ts`, `accessibility.spec.ts` |
    | `evals/evals.json` | Output-quality evals (schema v1, 6 cases) spanning authoring, scraping, debugging, and frontend test implementation |
    
    ## Quick Start
    
    ```bash
    # Inspect a Playwright suite and triage its runs — no node required
    bash scripts/pwrun doctor --json
    bash scripts/pwrun inventory --json
    bash scripts/pwrun report --report test-results/test-results.json --json
    
    # Scaffold a new suite (copy the templates into your project)
    cp templates/playwright.config.ts templates/example.spec.ts templates/accessibility.spec.ts .
    npm i -D @playwright/test
    npx playwright@1.62.1 install
    npx playwright@1.62.1 test
    ```
    
    The `--help` output documents every flag and works without Node. Set `BASE_URL` to override the smoke target; `scripts/pwrun smoke --url http://localhost:3000 --json` runs a delegated pass through `npx playwright@1.62.1 test`.
    
    ## Triggers
    
    Load this skill for Playwright, `playwright test`, E2E test authoring and debugging, flaky selectors/locators, network interception and mocking (`page.route`), test parallelism and sharding, running E2E tests in CI, accessibility snapshot checks (`toMatchAriaSnapshot`, axe scans), or scraping/headless browsing of JavaScript-rendered pages. Do not load it for QA strategy or framework selection (that's `qa-methodology`), frontend component design (that's `frontend-engineering`), or Cloudflare challenge bypass (that's `flaresolverr`).
    
    ## Requirements
    
    - Playwright 1.40+ for the documented patterns (aria snapshots need 1.49+).
    - Node.js + `@playwright/test` to run tests or the `smoke` command.
    - The `pwrun` script runs on Python 3.8+ (stdlib only) — `doctor`, `inventory`, and `report` need no Node or browser.
    - A display server (or headless mode) when running browsers on a CI runner.
    
  • SKILL.md 12.5 KB
    ---
    name: playwright
    description: >-
      Operate Playwright for browser automation end to end: author and debug E2E
      test suites (robust locators, network interception and mocking, parallel
      workers, accessibility snapshot checks), wire them into CI, and drive
      headless browsing and scraping with an extract -> validate -> save loop. Use
      when writing, running, fixing, or scraping with Playwright, when a Playwright
      CI failure or JSON report needs triage, or when the bundled pwrun script
      should analyze a run. Do not use for QA strategy or test framework selection
      (route to qa-methodology), for frontend component or architecture design
      (route to frontend-engineering), or for Cloudflare/DDoS-GUARD challenge
      bypass (use flaresolverr).
    license: MIT
    compatibility: >-
      Playwright 1.40+ for the documented patterns (aria snapshots need 1.49+).
      The bundled pwrun script runs on Python 3.8+ and needs no node or Playwright
      for --help, doctor, inventory, or report analysis; smoke delegates to npx.
    metadata:
      source: https://playwright.dev/docs
      spec: https://playwright.dev/docs/api/class-playwright
    ---
    
    # Playwright Browser Automation
    
    Use this skill to drive a real browser with Playwright: author end-to-end tests, keep selectors and tests robust, intercept and mock network traffic, run suites across parallel workers and in CI, scrape and extract data in headless mode, and check accessibility with snapshot scans. This is a **tool skill** for one named tool. Test strategy and framework selection belong to [qa-methodology](../qa-methodology/SKILL.md); frontend component and architecture design belong to [frontend-engineering](../frontend-engineering/SKILL.md). This skill owns operating the Playwright tool itself.
    
    ## Operating contract
    
    1. **Read the suite before running.** Inspect `playwright.config.*`, projects, `baseURL`, `webServer`, workers, retries, and reporters before running anything. Never assume the test command from the README.
    2. **Locate by behavior, not layout.** Prefer user-facing locators (`getByRole`, `getByLabel`, `getByText`) over CSS/XPath that encode markup. Tests coupled to user-visible behavior survive refactors; tests coupled to structure break on them.
    3. **Mock at the boundary.** Stub external HTTP at `page.route()` — never by patching in-page code. Mock the dependency under test's edges, never the code under test itself; a test that mocks what it claims to verify proves nothing.
    4. **Parallelize deliberately.** Playwright gives every test an isolated browser context. Tune `workers` and sharding to the machine and suite, and never let tests share mutable state through globals.
    5. **Verify at the boundary.** A green test is only as strong as its assertions. Assert on user-visible outcomes (visible text, URL, enabled/disabled state), not on implementation details.
    6. **Keep evidence bounded.** Capture traces, screenshots, and video on failure only; summarize JSON reports instead of dumping them; never paste full HTML dumps or session cookies into chat.
    
    ## The pwrun script
    
    `scripts/pwrun` is an agent-first smoke harness around a Playwright suite. `doctor`, `inventory`, and `report` work with no node or Playwright installed, so an agent can inspect a suite and triage a CI report anywhere.
    
    ```bash
    scripts/pwrun doctor --json                     # node, @playwright/test, browsers, config availability
    scripts/pwrun inventory --json                  # config, projects, spec files in the suite
    scripts/pwrun report --report test-results.json --json   # summarize a Playwright JSON report
    scripts/pwrun smoke --url http://localhost:3000 --json   # delegate a smoke run to npx playwright test
    ```
    
    Exit codes: 0 ok, 1 analysis error, 2 usage error, 127 dependency (node/playwright) missing, 124 delegate timeout. `--json` on every command; `--help` works without any toolchain.
    
    ## E2E test authoring
    
    - Structure suites with `test.describe` blocks, `test.beforeEach` setup, and per-feature fixture files. Keep specs short, focused on one user journey, and readable as prose.
    - Use Playwright's web-first assertions (`expect(locator).toBeVisible()`, `.toHaveText()`, `.toHaveURL()`) — they auto-retry until a timeout and are the backbone of stable E2E tests. Avoid manual `page.waitForTimeout` sleeps.
    - Start the app under test with `webServer` in the config (Playwright starts it, waits for readiness, and tears it down), or reuse an already-running instance via `baseURL` for smoke passes.
    - Authoring patterns, fixtures, and the page-object model: `references/01-e2e-authoring.md`.
    
    ## Selector robustness
    
    - Default to role, label, text, and placeholder locators — they describe the page the way a user does. Reserve CSS for layout-adjacent needs (e.g., `getByTestId` for ids that exist only for tests).
    - Chain and filter locators (`getByRole('row').filter({ hasText: 'Acme' })`) instead of building one long brittle selector. Re-query rather than storing stale element handles.
    - Fix flaky tests by finding which selector matched multiple or zero elements, not by adding sleeps or `first()`. A selector that can match the wrong thing will eventually.
    - Selector priority rules and flaky-selector repair flows: `references/02-selectors.md`.
    
    ## Network interception and mocking
    
    - Intercept requests with `page.route()` and fulfill, abort, or continue them. Use this to mock third-party APIs, inject fixtures, emulate offline or slow networks, and block analytics/trackers that pollute tests.
    - Mock at the route level with realistic bodies and content types. Never intercept the server the test is supposed to verify, and never route over `page.goto` navigation the test depends on.
    - Capture requests with `page.on('request')`/`page.on('response')` to assert on what the app actually sent. Patterns and gotchas: `references/03-network-interception-and-mocking.md`.
    
    ## Parallel workers
    
    - `workers` sets how many parallel processes run specs; `fullyParallel` lets every spec file run across workers. Scale to CPU count and browser memory, not to "as many as possible".
    - Shard suites across CI jobs (`--shard=1/4`) for large suites. Every test runs in its own browser context, so isolation is default — keep it that way: no shared globals, no shared storage state, no test-order coupling.
    - Worker counts, sharding math, and isolation traps: `references/04-parallel-workers-and-sharding.md`.
    
    ## CI integration
    
    - Install browsers and OS deps on the runner (`npx playwright@1.62.1 install --with-deps`), pin the Playwright version, and cache `~/.cache/ms-playwright`.
    - Configure `webServer`, `retries` (retry flaky tests on CI only), and `trace: 'on-first-retry'` so failures are debuggable. Report with `html`/`json`/`github` and upload artifacts on failure.
    - Triage CI failures from the JSON report with `scripts/pwrun report --json` — it summarizes stats, failing specs, and error messages without opening a browser. Full CI recipes: `references/05-ci-integration.md`.
    
    ## Scraping and headless browsing
    
    - Drive the browser API directly (`chromium.launch({ headless: true })`, `newContext`, `page.goto`) to load JavaScript-rendered pages, then extract with locators and `innerText`/attribute access into structured records — extract, validate, save.
    - Respect robots.txt, terms, and rate limits; bound the scrape by page count and delay. For Cloudflare/DDoS-GUARD challenge bypass, route to [flaresolverr](../flaresolverr/SKILL.md) — this skill does not solve challenges.
    - Scraping flows, pagination, and polite extraction: `references/06-scraping-and-headless.md`.
    
    ## Accessibility snapshot checks
    
    - Run full scans with `@axe-core/playwright` to catch WCAG violations (contrast, landmarks, ARIA misuse) in CI or on demand.
    - Use Playwright's aria snapshots (`expect(page).toMatchAriaSnapshot()`) as stable, accessibility-aware assertions: they compare the accessibility tree, so they catch structure and label regressions and read like spec assertions. Update deliberately, never `--update-snapshots` reflexively.
    - Scan setup, snapshot discipline, and fixing violations: `references/07-accessibility-and-debugging.md`.
    
    ## Headed debugging
    
    - Run headed (`--headed`), slow the action with `--slow-mo`, or drop into the inspector with `--debug` / `PWDEBUG=1` and the `page.pause()` breakpoint.
    - Generate starter tests with `npx playwright@1.62.1 codegen <url>`, then harden the generated selectors into user-facing locators.
    - When a test fails: read the trace (`--trace on`), which records network, DOM snapshots, and console for the failed action. Use `scripts/pwrun report --report <json> --json` first to see the failure summary.
    - Debugging workflows live in `references/07-accessibility-and-debugging.md`.
    
    ## Reference routing
    
    | Load when | Reference |
    |---|---|
    | Writing or structuring specs, fixtures, page objects | `references/01-e2e-authoring.md` |
    | A selector is flaky or matches the wrong element | `references/02-selectors.md` |
    | Mocking or intercepting API/network traffic | `references/03-network-interception-and-mocking.md` |
    | Speeding up or sharding a large suite | `references/04-parallel-workers-and-sharding.md` |
    | Wiring Playwright into CI or triaging CI failures | `references/05-ci-integration.md` |
    | Extracting data or browsing in headless mode | `references/06-scraping-and-headless.md` |
    | Accessibility scans, aria snapshots, or debugging a failing test | `references/07-accessibility-and-debugging.md` |
    | Sources, version observations, and refresh procedure | `references/00-source-index.md` |
    
    ## Included artifacts
    
    - `scripts/pwrun`: smoke harness — `doctor`, `inventory`, `report`, `smoke`, all with `--json`.
    - `tests/test_pwrun.py` + `tests/fixtures/sample-report.json`: deterministic tests for the harness (no node/browser required).
    - `templates/playwright.config.ts`, `templates/example.spec.ts`, `templates/accessibility.spec.ts`: copy-in test-suite scaffold.
    - `references/`: eight dated, source-indexed references covering the operational topics above.
    
    ## Verification boundary
    
    | Claim | Minimum evidence |
    |---|---|
    | Toolchain is present | `scripts/pwrun doctor --json` reports node, @playwright/test, and browsers available |
    | Suite is understood | `scripts/pwrun inventory --json` lists config and spec files |
    | A test passes | `npx playwright@1.62.1 test` exit 0 on the targeted spec (or `smoke` delegation) |
    | A CI failure is explained | `scripts/pwrun report --report test-results.json --json` names the failing specs and errors |
    | Accessibility is covered | An axe scan runs with zero violations of the declared severity, and aria snapshots match |
    | No regressions in the covered flows | The suite ran under the configured workers/sharding with expected/flaky/unexpected counts recorded |
    
    ## Hard boundaries
    
    - Never mock the code under test to force a green test — mock only its boundaries.
    - Never scrape in violation of robots.txt, terms, or rate limits; never harvest credentials or personal data, and never extract auth/session storage into committed files.
    - Never commit `playwright/.auth/*` storage state, `.env`, or browser credentials.
    - Never run `--update-snapshots` blindly to "fix" an aria snapshot diff — inspect what changed first.
    - Never dump raw HTML, full trace files, or screenshots of protected content into chat; summarize with `pwrun report` instead.
    - A headed browser in CI needs a display server (e.g., xvfb) — never assume a display exists on a runner.
    
    ## When not to use
    
    - **Test strategy, framework selection, or QA process** — route to [qa-methodology](../qa-methodology/SKILL.md).
    - **Frontend component/state/architecture design or implementation guidance** — route to [frontend-engineering](../frontend-engineering/SKILL.md); React-specific component and hooks work routes to [react](../react/SKILL.md).
    - **Cloudflare/DDoS-GUARD challenge bypass** — route to [flaresolverr](../flaresolverr/SKILL.md).
    - **Load/performance testing at scale** (k6, Locust, Gatling, JMeter) — that methodology lives under `qa-methodology`'s performance-testing reference.
    - **Raw HTTP retrieval of static content** — use a plain HTTP client; Playwright is for JavaScript-rendered pages and browser workflows.
    
    ## Topic coverage keywords
    
    The catalog's automated topic sweep greps this file for coverage keywords. Two
    alias pairs are written literally below so the sweep matches both spellings:
    `e2e|end-to-end` covers browser-level tests (E2E, end-to-end, and e2e are the
    same workflow), and `debug|head` covers the headed-debugging workflows in this
    skill. All other topics (selector robustness, network interception, parallel
    workers, CI integration, scraping, accessibility snapshots, mocking) appear by
    name in the sections above.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related