---
overlay: Playwright Testing
parent_agent: Super Tester
description: "Browser-based E2E testing with Playwright"
---

## PLAYWRIGHT BROWSER TESTING GUIDELINES

You are testing a **web application** using Playwright via the `playwright-cli` tool. This overlay provides battle-tested patterns for browser-based E2E testing.

**Command discovery:** Run `playwright-cli --help` for all commands. Run `playwright-cli --help <command>` for details on any specific command.

**ALWAYS clean up when done:** `playwright-cli session-stop-all`

---

### BOOTSTRAP

Run this ONCE at the start of any browser testing session:

```bash
which playwright-cli || npm install -g @anthropic-ai/playwright-cli@latest
PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true npx playwright install chromium
playwright-cli session-stop 2>/dev/null    # Kill stale sessions from crashed agents
playwright-cli config --browser=chromium
```

**Why each step matters:**
- `which` check — skip reinstall if already present
- `PLAYWRIGHT_SKIP_*` — prevents false install failures in containers and non-standard environments
- `session-stop` — a stale session from a previous run blocks everything. This is silent if nothing is running
- `config` — ensures chromium is the active browser

After bootstrap, `playwright-cli open <url>` will work. The session is a background daemon that persists between commands.

---

### TRAPS THAT WILL DERAIL YOU

Read these BEFORE your first command. These come from real testing sessions where assumptions failed. Each one will save you from getting stuck.

#### TRAP 1: Element refs die after ANY page change
When you run `snapshot`, you get refs like `e1`, `e23`. These refs are tied to THAT specific snapshot. After `open`, `click` that navigates, `hover`, `reload`, `tab-select`, `go-back`, or ANY action that changes the page — ALL refs are stale.

**The scariest case:** After a session restart, the same ref number (e.g. `e426`) can point to a COMPLETELY DIFFERENT element. No error — you just interact with the wrong thing.

**Rule:** After any action that might change the page, take a new `snapshot` before using refs. The safe pattern is always: `action → snapshot → use new refs`.

#### TRAP 2: `tab-new <url>` does NOT navigate
```bash
tab-new https://example.com
# Result: new tab opens at about:blank — NOT example.com
```
This is a silent failure — no error. You'll snapshot `about:blank` and wonder why the page is empty.

**Fix:** Always use two steps:
```bash
tab-new
open https://example.com
```

#### TRAP 3: `close` KILLS the entire session
```bash
close
# Result: "Session 'default' stopped." — ALL cookies, localStorage, browser state GONE
```
If you use `close` thinking you're closing a tab, you destroy everything. Any login state, any test setup — gone.

**Fix:** NEVER use `close`. Use `tab-close <index>` to close individual tabs. Use `session-stop` when you're truly done.

#### TRAP 4: console and network return FILE PATHS, not content
```bash
console error
# Output: "- [Console](.playwright-cli/console-xxx.log)"
```
The path IS the output. You MUST read that file to see the actual errors.

#### TRAP 5: Snapshots don't show form values or focus state
You filled a form with `fill e53 "John"`. The snapshot YAML will NOT show "John" in the field. You cannot verify form state by reading the snapshot.

**Fix for form values:** `eval "(el) => el.value" e53` — returns "John"
**Fix for focus:** `eval "() => document.activeElement?.tagName"` — snapshots never show which element has keyboard focus.

#### TRAP 6: eval only fails on DOM nodes
Returning DOM elements gives useless `"ref: <Node>"`. But primitives, plain objects, and arrays all work fine:
```bash
eval "() => 42"                                    # works: 42
eval "() => document.title"                        # works: "My Page"
eval "() => ({ links: 92, images: 14 })"           # works: { links: 92, images: 14 }
eval "() => [...document.querySelectorAll('a')].map(a => a.href)"  # works: ["url1", ...]
eval "() => document.querySelectorAll('a')"        # FAILS: { "0": "ref: <Node>", ... }
```
**Rule:** Don't wrap everything in `JSON.stringify()` — only use `.map()` to extract data from NodeLists.

#### TRAP 7: Multi-tab "Page URL" header is WRONG
When using multiple tabs, the "Page" section in snapshot output shows the WRONG tab's URL. Only the "Open tabs" section correctly shows which tab is `(current)`.

**Fix:** To verify your current URL in multi-tab mode:
```bash
eval "() => window.location.href"
```

#### TRAP 8: Soft 404s — HTTP 200 on error pages
Many SPAs and Next.js sites serve 404 pages with HTTP 200 status:
```bash
run-code 'async (page) => { const r = await page.goto("https://example.com/nonexistent"); return r.status(); }'
# Returns: 200 — even though the page says "Not Found"
```

**Fix:** Check page content, not HTTP status:
```bash
eval "() => document.title"                          # "Page Not Found | MySite"
eval "() => document.querySelector('h1')?.textContent"  # "Lost your way?"
```

#### TRAP 9: Dialogs block EVERYTHING
If a site triggers an `alert()`, `confirm()`, or `prompt()`, ALL other commands fail:
```
Error: Tool "browser_click" does not handle the modal state.
```

**Fix:** The CLI output will show a `### Modal state` section telling you exactly what dialog is open. Dismiss it before doing anything else:
```bash
dialog-accept              # OK/Accept
dialog-accept "value"      # Accept with text (for prompt dialogs)
dialog-dismiss             # Cancel/Dismiss
```

#### TRAP 10: `type` and `fill` are fundamentally different
- `fill <ref> <text>` — targets a specific element by ref, REPLACES all content, uses Playwright's `locator.fill()`
- `type <text>` — types into whatever has focus (NO ref), APPENDS to existing content, uses `keyboard.type()`

**Default to `fill` for form testing.** It's more reliable and doesn't depend on focus state. Use `type` only for keyboard-specific behavior testing.

#### TRAP 11: `fill --submit` fills THEN presses Enter
```bash
fill e53 "test@example.com" --submit
# Fills the field, then immediately presses Enter
```
This is a shortcut for fill + submit. Useful for search fields and login forms.

#### TRAP 12: run-code quote escaping
Single quotes for outer wrapper, double quotes inside:
```bash
# CORRECT:
run-code 'async (page) => { await page.locator("h1").textContent(); }'

# WRONG (shell eats the quotes):
run-code "async (page) => { await page.locator('h1').textContent(); }"
```

#### TRAP 13: Tab index shifts after tab-close
When you close tab 1, the former tab 2 becomes tab 1. Use `tab-list` after closing to confirm the new order.

#### TRAP 14: network --static vs default
```bash
network         # Only dynamic requests (API calls, XHR, fetch)
network --static   # ALL resources (CSS, JS, fonts, images too)
```
Use `--static` for full resource audits. Use default for API call monitoring.

#### TRAP 15: Playwright auto-scrolls for you
You do NOT need to scroll to an element before clicking, filling, or hovering. Playwright scrolls to the target automatically. Only scroll manually for:
- Checking what's "above the fold" at different scroll positions
- Testing lazy-loaded content
- Taking viewport screenshots at specific positions
- Testing infinite scroll behavior

---

### THE CORE TESTING LOOP

Every browser test follows this loop. Internalize it.

```
open <url>
 ↓
[health check: console error + network for 4xx/5xx]
 ↓
snapshot → get refs → interact (click, fill, etc.)
 ↓
[after any page change: snapshot again → get new refs]
 ↓
screenshot + eval → capture evidence
 ↓
repeat for next test step
```

**The golden rule:** After ANY action that might change the page (click, navigate, hover, reload, tab-select, go-back), take a new `snapshot` before using refs.

#### Health Check Pattern
Run this immediately after opening any page:

```bash
open https://example.com
console error                          # Get the log file path
# READ that file — look for JS errors
network                                # Get the network log path
# READ that file — look for failed requests (4xx, 5xx)
```

**Use `--clear` to isolate phases:**
```bash
console --clear   # Clear log before next test phase
network --clear   # Clear log before next test phase
```

---

### TAB-BASED TESTING — THE GOLD PATTERN

Instead of creating separate browser sessions for each test scenario, use tabs within one session. This is memory-efficient, preserves shared state (cookies, localStorage), and gives you a clear "done" signal.

#### Why tabs are superior
1. **One browser, multiple contexts** — 10 tabs use far less memory than 10 sessions
2. **Shared state** — cookies and localStorage persist across tabs, like real users
3. **Progress tracking** — open tabs = remaining work, closed = done
4. **Easy comparison** — `tab-select` between viewports instantly

#### The tab workflow
```bash
# Tab 0: Your home base (desktop, light mode)
open https://example.com
screenshot --full-page --filename=desktop-light.png

# Tab 1: Mobile
tab-new
open https://example.com              # ALWAYS open after tab-new!
resize 375 812
screenshot --full-page --filename=mobile-light.png

# Tab 2: Desktop dark mode
tab-new
open https://example.com
run-code 'async (page) => { await page.emulateMedia({ colorScheme: "dark" }); }'
screenshot --full-page --filename=desktop-dark.png

# Tab 3: Mobile dark mode
tab-new
open https://example.com
resize 375 812
run-code 'async (page) => { await page.emulateMedia({ colorScheme: "dark" }); }'
screenshot --full-page --filename=mobile-dark.png
```

#### Working with tabs
```bash
tab-list                    # See all tabs with indexes and URLs
tab-select <index>          # Switch to a specific tab
tab-close <index>           # Close a tab (NOT close! close kills the session)
```

#### Tab completion signal
- All test tabs open = all scenarios in progress
- Tab closed = that scenario is complete
- Only tab 0 remains = ALL testing complete

#### Critical tab gotchas
- `tab-new <url>` opens about:blank — ALWAYS follow with `open <url>`
- "Page URL" in snapshot is wrong when multiple tabs are open — trust "Open tabs" section
- After `tab-close`, remaining tab indexes shift — use `tab-list` to re-orient
- NEVER use `close` — it kills the session. ALWAYS use `tab-close <index>`

---

### MULTI-VIEWPORT TESTING

#### Standard breakpoints
| Device | Width | Height |
|--------|-------|--------|
| Desktop | 1280 | 720 |
| Tablet | 768 | 1024 |
| Mobile | 375 | 812 |

#### Single-page approach (when you just need screenshots)
```bash
open https://example.com
screenshot --full-page --filename=desktop.png

resize 768 1024
screenshot --full-page --filename=tablet.png

resize 375 812
screenshot --full-page --filename=mobile.png

resize 1280 720   # Reset to desktop
```

#### Multi-tab approach (when you need to compare or interact)
Use the tab workflow above — one tab per viewport. This lets you switch between viewports to compare specific elements.

#### What to check at each viewport
1. **Layout** — does content reflow properly? No horizontal overflow?
   ```bash
   eval "() => ({ hasHScroll: document.body.scrollWidth > window.innerWidth })"
   ```
2. **Navigation** — is the hamburger menu working? Are nav items accessible?
3. **Text** — is text readable? No truncation?
4. **Interactive elements** — are buttons large enough to tap? No overlapping clickables?
5. **Images** — do they scale properly? Any broken images?

#### Automated breakpoint sweep (for comprehensive audits)
```bash
run-code 'async (page) => {
  const breakpoints = [320, 375, 425, 768, 1024, 1280, 1440, 1920];
  for (const w of breakpoints) {
    await page.setViewportSize({ width: w, height: 800 });
    await page.screenshot({ fullPage: true, path: `.playwright-cli/responsive-${w}.png` });
  }
  return "Done: " + breakpoints.length + " screenshots at all breakpoints";
}'
```

---

### DARK MODE TESTING

Dark mode is a standard feature now. When testing UI, you should check both light and dark variants.

#### Approach hierarchy — try in this order
Different sites implement dark mode differently. Try these approaches in order until one works:

**1. System preference emulation (MOST RELIABLE — works for standards-compliant sites):**
```bash
run-code 'async (page) => { await page.emulateMedia({ colorScheme: "dark" }); }'
```

**2. Check if emulation worked — if not, try class-based themes:**
```bash
# Check what CSS classes the site uses
eval "() => document.documentElement.className"
# If Tailwind/Next.js themes: add the class
eval "() => document.documentElement.classList.add('dark')"
# Or data-attribute based:
eval "() => document.documentElement.setAttribute('data-theme', 'dark')"
```

**3. If no class-based theme, look for a toggle in the snapshot:**
```bash
snapshot
# Find a theme toggle button, then click it
click <toggle-ref>
```

**4. If no toggle visible, check localStorage:**
```bash
eval "() => JSON.stringify(localStorage)"
# Look for theme-related keys and set them:
eval "() => { localStorage.setItem('theme', 'dark'); location.reload(); }"
```

#### The four-screenshot matrix
For comprehensive visual testing, capture all combinations:

| Viewport | Theme | Filename |
|----------|-------|----------|
| Desktop (1280x720) | Light | `desktop-light.png` |
| Desktop (1280x720) | Dark | `desktop-dark.png` |
| Mobile (375x812) | Light | `mobile-light.png` |
| Mobile (375x812) | Dark | `mobile-dark.png` |

Use the tab workflow to set up all four scenarios efficiently.

---

### SCREENSHOT STRATEGY

#### Three screenshot modes

**1. Full page** — entire scrollable page in one image:
```bash
screenshot --full-page --filename=homepage-full.png
```
Use for: visual baselines, layout overview, content audit, initial assessment.

**2. Viewport only** — just what's visible in current viewport:
```bash
screenshot --filename=above-the-fold.png
```
Use for: above-the-fold checks, specific sections after scrolling, detail inspection.

**3. Element** — just one element:
```bash
screenshot e426 --filename=login-form.png
```
Use for: form states, button styles, component-level comparisons. Saved as `element-*.png`.

#### Use `--filename` for predictable names
Without it, screenshots get auto-generated timestamps. Named files are easier to reference in reports.

#### Scroll-and-snap pattern (fold-by-fold inspection)
```bash
screenshot --filename=fold-1.png
mousewheel 0 720
screenshot --filename=fold-2.png
mousewheel 0 720
screenshot --filename=fold-3.png
```

---

### FORM TESTING

#### The reliable pattern: fill → verify → screenshot → submit

```bash
# 1. Fill all fields using fill (not type)
fill e53 "John"
fill e56 "Doe"
fill e59 "john@example.com"
fill e62 "Acme Corp"

# 2. Verify values were set (snapshots DON'T show form values!)
eval "() => {
  const inputs = document.querySelectorAll('input[type=text], input[type=email]');
  return Array.from(inputs).map(i => ({ name: i.name, value: i.value }));
}"

# 3. Screenshot the filled form (visual evidence)
screenshot --filename=form-filled.png

# 4. Submit
click e64                              # Click submit button
# Or: fill e62 "Acme Corp" --submit   # Fill last field + press Enter
```

#### When to use `fill` vs `type`
- **fill** — for setting form field values. Cleaner, more reliable, targets by ref, REPLACES content
- **type** — for testing keyboard behavior. Appends to focused element, tests autocomplete triggers, input event handlers

#### Verifying form state
**Snapshots don't show input values.** You MUST use eval:
```bash
eval "(el) => el.value" e53            # Check one field
eval "(el) => el.checked" e72          # Check checkbox
```

#### Select dropdowns and file uploads
```bash
select e80 "option-value"              # Select dropdown option
upload /path/to/file.pdf               # Upload a file
```

---

### EVAL & RUN-CODE — YOUR POWER TOOLS

#### eval: quick data extraction

Without ref — runs on page (window context):
```bash
eval "() => document.title"
eval "() => window.location.href"
eval "() => document.querySelectorAll('a').length"
```

With ref — function receives that element:
```bash
eval "(el) => el.value" e53                    # Input value
eval "(el) => getComputedStyle(el).fontSize" e156  # CSS property
eval "(el) => el.getBoundingClientRect()" e9   # Dimensions
eval "(el) => el.getAttribute('aria-label')" e42  # Attribute
```

#### run-code: full Playwright API

When the CLI commands aren't enough, `run-code` gives you the full `page` object:

**Response interception:**
```bash
run-code 'async (page) => {
  let calls = [];
  page.on("response", r => {
    if (r.url().includes("api")) calls.push({ url: r.url(), status: r.status() });
  });
  await page.reload();
  await page.waitForTimeout(2000);
  return calls;
}'
```

**Wait for specific conditions:**
```bash
run-code 'async (page) => {
  await page.waitForSelector(".loading-spinner", { state: "hidden" });
  return "page loaded";
}'
```

**Cookie manipulation:**
```bash
run-code 'async (page) => {
  const ctx = page.context();
  await ctx.addCookies([{ name: "test", value: "123", url: "https://example.com" }]);
  return "cookie set";
}'
```

**Rule:** Use CLI commands for common operations (click, fill, screenshot). Use `run-code` for things CLI commands can't do.

---

### VERIFICATION PATTERNS

When you need to verify something, don't guess — use the right method.

#### Verifying navigation succeeded
```
1. eval "() => window.location.href"           ← ground truth
2. Check "Open tabs" section (NOT "Page URL" header in multi-tab mode)
3. eval "() => document.title"
```

#### Checking for errors
```
1. Check page content (catches soft 404s):
   eval "() => document.title"

2. Check HTTP status (catches hard errors):
   run-code 'async (page) => { const r = await page.goto(url); return r.status(); }'

3. Check console for JavaScript errors:
   console error → read the log file
```

#### Verifying an element exists and is visible
```
1. Check snapshot for the element
2. If not in snapshot: eval "() => document.querySelector('#myElement') !== null"
3. Check visibility: eval "(el) => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; }" <ref>
```

#### Verifying form submission worked
```
1. Check URL changed: eval "() => window.location.href"
2. Check for success message in snapshot
3. Check network log for the submission request
4. Check console for errors
```

---

### WHEN THINGS GO WRONG — SELF-RECOVERY

These are the patterns where agents derail. If you find yourself stuck, check this section.

#### "Ref not found" errors
**Cause:** Refs are stale. You did something that changed the page since your last snapshot.
**Fix:** Run `snapshot` to get fresh refs. Then use the new refs.

#### All commands fail with "does not handle modal state"
**Cause:** A dialog (alert/confirm/prompt) is blocking the page.
**Fix:** Run `dialog-accept` or `dialog-dismiss`. Then continue.

#### Page is blank / empty snapshot
**Cause 1:** You used `tab-new` without `open`. Tab is at `about:blank`.
**Fix:** Run `open <url>`.

**Cause 2:** Page is still loading.
**Fix:** `run-code 'async (page) => { await page.waitForSelector("body > *"); return "loaded"; }'`

#### Session died unexpectedly
**Cause:** You used `close` instead of `tab-close`, or the session crashed.
**Fix:** Run the bootstrap sequence again:
```bash
playwright-cli session-stop 2>/dev/null
playwright-cli config --browser=chromium
playwright-cli open <url>
```

#### Command behaves unexpectedly
**Fix:** Run `playwright-cli --help <command>` to see the exact syntax and flags. Don't assume — verify.

#### You're stuck in a loop
**Fix:** Stop. Use `sequential_thinking` to analyze what went wrong. Often the issue is stale refs, a dialog, or wrong URL.

---

### VIDEO & TRACING

#### Video recording (for human stakeholders)
```bash
video-start          # Start recording
# ... do your testing ...
video-stop           # Stop and save → .playwright-cli/video-*.webm
```
Video is for humans — LLMs can't process .webm files. For your own analysis, prefer screenshots + text notes at key moments.

#### Tracing (for performance debugging)
```bash
tracing-start        # Start trace
# ... do actions ...
tracing-stop         # Stop → .playwright-cli/traces/trace-*.trace
```

#### PDF generation (for print testing)
```bash
pdf                  # Save page as PDF → .playwright-cli/page-*.pdf
```
Only works in Chromium. Uses print stylesheet. Useful for testing print layouts or generating report artifacts.

---

### PLAYWRIGHT-SPECIFIC RULES

#### ALWAYS
- Run `snapshot` after any page-changing action before using refs
- Use `fill` (not `type`) for setting form field values
- Use `tab-close <index>` (NEVER `close`) to close tabs
- Use `eval` to verify form values and current URL (not snapshot text)
- Use `--help <command>` when unsure about syntax
- Clean up: `session-stop-all` when done

#### NEVER
- Use stale refs — always re-snapshot after page changes
- Trust "Page URL" in snapshot when multiple tabs are open
- Trust HTTP status codes for 404 detection on SPAs
- Use `close` to close a tab — it kills the session
- Assume `tab-new <url>` navigates — it opens about:blank

#### SELF-CHECK
If you're stuck or confused:
1. Check for dialogs blocking you (`dialog-accept` / `dialog-dismiss`)
2. Check if your refs are stale (run `snapshot`)
3. Check which tab you're on (`tab-list` + `eval "() => window.location.href"`)
4. Check `--help <command>` for correct syntax
5. Use `sequential_thinking` to step back and analyze
