# Browser Testing

> Hand-driven end-to-end browser testing through the Playwright MCP server — navigating pages, filling forms, asserting HTTP responses, capturing screenshots for visual regression, checking responsive breakpoints and cross-browser behaviour, and recording codegen sessions. Use when a browser genuinely has to be driven interactively and no runnable suite covers the need — authoring e2e, screenshot, or visual-verification coverage where none exists, debugging a failure a suite already surfaced, or exploring a live page. It does not cover running a repository's existing suite. When the repo has a playwright.config file, a tests or e2e directory, or a test:e2e script, running that suite is the correct action and this skill is not required.

- Skill: `pfangueiro/browser-testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pfangueiro/browser-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pfangueiro/browser-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: pfangueiro (https://skillmd.com/u/pfangueiro)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pfangueiro/browser-testing

---


# Browser Testing with Playwright MCP

Comprehensive patterns for end-to-end browser testing, visual regression, and responsive design verification using the Playwright MCP server.

## First: prefer the repo's own test suite

This skill is for hand-driving a browser when no runnable suite covers the need. It deliberately does NOT claim the bare word "Playwright" — running an existing suite is the correct action. Before hand-driving any MCP tool, check for one:

- `playwright.config.{ts,js,mjs}`, a `tests/` / `e2e/` directory, or a `test:e2e` / `playwright` script in `package.json`.

If a suite exists, **run it** (the repo's script, or `npx playwright test`) and report its actual result. A suite is reproducible, runs in CI, and asserts far more than a hand-driven MCP session can. Use the MCP tools below only to (a) debug a failure the suite surfaced, (b) explore a page interactively, or (c) build coverage where no suite exists.

Do not re-implement an existing test by hand, and never present a hand-driven MCP walkthrough as equivalent to a suite run — if the suite was not run, say so.

## Available Playwright MCP Tools

Names below are the full MCP tool names, as invoked.

### Navigation & State
- `mcp__playwright__playwright_navigate` — Open a URL (set `browserType`, `width`, `height`, `headless`)
- `mcp__playwright__playwright_go_back` / `mcp__playwright__playwright_go_forward` — History navigation
- `mcp__playwright__playwright_resize` — Change viewport (manual or 143+ device presets: `iPhone 13`, `iPad Pro`, `Galaxy S24`, etc.)

### Interaction
- `mcp__playwright__playwright_click` — Click an element (CSS selector)
- `mcp__playwright__playwright_fill` — Fill an input field
- `mcp__playwright__playwright_select` — Select dropdown value
- `mcp__playwright__playwright_hover` — Hover over element
- `mcp__playwright__playwright_drag` — Drag element to target
- `mcp__playwright__playwright_press_key` — Press keyboard key
- `mcp__playwright__playwright_upload_file` — Upload file to input

### Inspection
- `mcp__playwright__playwright_screenshot` — Capture page or element screenshot (with `fullPage`, `selector`, `savePng`, `downloadsDir`)
- `mcp__playwright__playwright_get_visible_text` — Get all visible text on page
- `mcp__playwright__playwright_get_visible_html` — Get HTML (options: `cleanHtml`, `minify`, `removeScripts`, `selector`)
- `mcp__playwright__playwright_console_logs` — Retrieve browser console logs (filter by type: error, warning, etc.)
- `mcp__playwright__playwright_evaluate` — Execute arbitrary JavaScript in browser

### HTTP API Testing
- `mcp__playwright__playwright_get` / `mcp__playwright__playwright_post` / `mcp__playwright__playwright_put` / `mcp__playwright__playwright_patch` / `mcp__playwright__playwright_delete` — Direct HTTP operations with auth headers

### Code Generation
These four are the only tools with no `playwright_` infix — the name is `mcp__playwright__start_codegen_session`, not `..._playwright_start_codegen_session`.

- `mcp__playwright__start_codegen_session` — Begin recording interactions as Playwright test code (requires `options.outputPath`)
- `mcp__playwright__end_codegen_session` — Generate the test file from recorded actions (requires `sessionId`)
- `mcp__playwright__get_codegen_session` — Check session status (requires `sessionId`)
- `mcp__playwright__clear_codegen_session` — Discard without generating (requires `sessionId`)

### Response Validation
- `mcp__playwright__playwright_expect_response` — Start waiting for an HTTP response. Required: `id` (an arbitrary handle you choose) and `url` (pattern). Registers the wait; does not block.
- `mcp__playwright__playwright_assert_response` — Await that response. Required: `id`. Optional: `value` — a substring that must appear in the JSON body.

**Neither tool can assert an HTTP status code — there is no status parameter.** Two consequences, both fail-closed:

- **Never call `assert_response` with only an `id`.** With no `value`, the server reports success for *any* response it received: a 401, 403 or 500 comes back as "Response assertion ... successful" with the status merely *printed* in the message, not checked. A failed login then reads as a green E2E. Always pass `value`.
- **`value` must be a substring that appears ONLY on success** (a token field name, the user id, `"status":"ok"`). A string that also occurs in the error body proves nothing. If no success-only substring exists, this tool cannot verify the call — assert the outcome another way (e.g. `mcp__playwright__playwright_get_visible_text` for an element that renders only when authenticated) and report the response as *not verified* rather than as a pass.

`value` is matched as a plain substring against `JSON.stringify(body)`, and the body is parsed as JSON — a non-JSON response makes the assertion error out.

## Patterns

### E2E Test: User Journey
```
1. mcp__playwright__playwright_navigate → target URL
2. mcp__playwright__playwright_fill → username/password fields
3. mcp__playwright__playwright_expect_response → id: "auth", url: "**/api/auth**"
   (MUST precede the click — the wait is registered here; a response that
    fires before this call is never seen)
4. mcp__playwright__playwright_click → submit button
5. mcp__playwright__playwright_assert_response → id: "auth", value: "\"authenticated\":true"
   (`value` is mandatory — with only `id`, a 401 is reported as success)
6. mcp__playwright__playwright_screenshot → capture authenticated state
7. mcp__playwright__playwright_get_visible_text → verify welcome message
```

Replace `"\"authenticated\":true"` with whatever substring your auth endpoint returns **only** on success.

### Visual Regression: Screenshot Comparison

**The Playwright MCP server has no image comparator** — no baseline store, no diff, no threshold. `mcp__playwright__playwright_screenshot` only captures. The comparison step must be an external tool, or the comparison is not happening.

```
1. mcp__playwright__playwright_resize → pin an exact viewport (both runs must match)
2. mcp__playwright__playwright_navigate → target page
3. mcp__playwright__playwright_screenshot → name: "homepage", savePng: true,
     downloadsDir: "<repo>/.screenshots/baseline"
4. [Make code changes]
5. mcp__playwright__playwright_navigate → same page (fresh load)
6. mcp__playwright__playwright_screenshot → name: "homepage", savePng: true,
     downloadsDir: "<repo>/.screenshots/current"
7. Compare — but assert the dimensions FIRST (see the size-mismatch rule below):
     B=.screenshots/baseline/homepage.png; C=.screenshots/current/homepage.png
     db=$(magick identify -format '%wx%h' "$B") ; dc=$(magick identify -format '%wx%h' "$C")
     [ -n "$db" ] && [ -n "$dc" ] || { echo "FAIL: cannot read dimensions"; exit 1; }
     [ "$db" = "$dc" ] || { echo "FAIL: size mismatch $db vs $dc — comparison invalid"; exit 1; }
     compare -metric AE "$B" "$C" diff.png     # differing-pixel count; needs ImageMagick
   NOTE the filenames: mcp__playwright__playwright_screenshot may write `<name>-<timestamp>.png` rather than
   `<name>.png`. Resolve the actual path (e.g. `ls -t .screenshots/current/homepage*.png | head -1`)
   instead of assuming — a comparator pointed at a path that does not exist must FAIL, not pass.
```

Fail-closed rules:

- **Decide the pass threshold before running.** "Looks fine" is not a threshold.
- **A missing baseline is a FAIL, not a pass.** Recording a first baseline is a separate, deliberate step — never let "nothing to compare against" report green.
- **Pin the viewport and use the same `fullPage` value for both captures, and assert the two images
  have identical dimensions before comparing.** Do **not** rely on the comparator to notice. Measured
  on ImageMagick 7.1.2-21: `compare -metric AE` on a 200×200 vs a 100×100 image returns
  **`0 (0)` with exit 0** — a *perfect pass* — while a genuine same-size regression returns
  `2601 (0.065)` with exit 1. A size mismatch is therefore the cleanest possible green, which is the
  opposite of erroring out. Compare `magick identify -format '%wx%h'` on both files and FAIL on any
  difference. (Earlier revisions of this file asserted the mismatch "makes the comparator error out";
  that was wrong, and it was wrong in the direction that hides regressions.)
- **A comparator that did not run, is not installed, or errored is a FAIL.** Report "not verified" — never downgrade it to a pass.
- **Never report a visual-regression pass from eyeballing two screenshots.** Model inspection of two PNGs is not a diff; if no comparator ran, the honest result is "not verified".

### Responsive Design Testing
```
1. mcp__playwright__playwright_navigate → target page
2. mcp__playwright__playwright_resize → device: "iPhone 13" (375x812)
3. mcp__playwright__playwright_screenshot → name: "mobile"
4. mcp__playwright__playwright_resize → device: "iPad Pro 11" (834x1194)
5. mcp__playwright__playwright_screenshot → name: "tablet"
6. mcp__playwright__playwright_resize → width: 1920, height: 1080
7. mcp__playwright__playwright_screenshot → name: "desktop"
```

### Form Validation Testing
```
1. mcp__playwright__playwright_navigate → form page
2. mcp__playwright__playwright_fill → invalid email
3. mcp__playwright__playwright_click → submit
4. mcp__playwright__playwright_get_visible_text → check for error messages
5. mcp__playwright__playwright_console_logs → type: "error" (check for JS errors)
6. mcp__playwright__playwright_fill → valid data
7. mcp__playwright__playwright_expect_response → id: "submit", url: "**/api/submit**"
   (register the wait BEFORE the click that triggers it)
8. mcp__playwright__playwright_click → submit
9. mcp__playwright__playwright_assert_response → id: "submit", value: "\"created\":true"
   (success-only substring; omitting `value` would pass on a 422 validation error)
```

### Codegen: Record Test from Manual Actions
```
1. mcp__playwright__start_codegen_session → options: { outputPath: "/tests/e2e/" }
2. mcp__playwright__playwright_navigate → target URL
3. mcp__playwright__playwright_click → interact with elements
4. mcp__playwright__playwright_fill → fill forms
5. mcp__playwright__end_codegen_session → sessionId from step 1; writes the test file to outputPath
```

### Cross-Browser Testing
```
1. mcp__playwright__playwright_navigate → url, browserType: "chromium"
2. mcp__playwright__playwright_screenshot → name: "chrome-result"
3. mcp__playwright__playwright_close
4. mcp__playwright__playwright_navigate → url, browserType: "firefox"
5. mcp__playwright__playwright_screenshot → name: "firefox-result"
6. mcp__playwright__playwright_close
7. mcp__playwright__playwright_navigate → url, browserType: "webkit"
8. mcp__playwright__playwright_screenshot → name: "safari-result"
```

## Best Practices

- Run the repo's existing Playwright suite before hand-driving MCP tools (see the gate at the top)
- Always `mcp__playwright__playwright_close` when done — releases browser resources
- Use `selector` option on screenshots to capture specific components
- Set `headless: true` for CI/automated runs, `false` for debugging
- Use `mcp__playwright__playwright_console_logs` with `type: "error"` after interactions to catch JS errors
- Register `mcp__playwright__playwright_expect_response` BEFORE the action that triggers the request
- Always pass `value` to `mcp__playwright__playwright_assert_response` — without it, a 401/500 passes
- Use device presets for accurate mobile/tablet testing (includes user-agent + touch emulation)
- For iframes, use `mcp__playwright__playwright_iframe_click` and `mcp__playwright__playwright_iframe_fill`

## When to Use

- **After implementing UI changes** — verify visual correctness
- **During investigate** — reproduce UI bugs with exact browser state
- **For accessibility** — check WCAG compliance with visual + text inspection
- **For API testing** — use HTTP tools for direct endpoint verification
- **For regression** — screenshot before/after code changes

## Related

- `test-automation` agent — generates test suites (uses Playwright for E2E)
- `frontend-specialist` agent — implements UI components (uses Playwright for verification)
- `investigate` skill — Phase 2 (REPRODUCE) uses Playwright for UI bug reproduction

