build(web): replace Playwright MCP with project-scoped Playwright CLI

- feat: add `.agents/skills/playwright-cli/` with complete Skill
  markdown, agent interface, and 9 reference files covering session
  management, spec-driven testing, video recording, tracing, storage,
  request mocking, element attributes, test debugging, and running
  custom code
- feat: add `.playwright/cli.config.json` to configure the Playwright
  CLI
- feat: add `web/package.json` `playwright:cli` script that resolves
  `playwright cli` from the repo root
- build: remove `@playwright/mcp` dependency from `web/package.json`
- build: set `XDG_CACHE_HOME` in `mise.toml`
- build: delete `.codex/config.toml` (MCP server config)
- docs: update `README.md`, `docs/web/roadmap.md`,
  `docs/web/decisions.md`, and `web/README.md` to reflect the new
  Playwright CLI approach
This commit is contained in:
2026-07-15 23:10:43 +08:00
parent 29b176d2db
commit 52dd56ff06
21 changed files with 1793 additions and 113 deletions
+107
View File
@@ -0,0 +1,107 @@
---
name: playwright-cli
description: Debug and automate the MyGO Web interface with the repository-pinned Playwright CLI. Use for headless browser inspection, login and file-flow debugging, screenshots, DOM snapshots, console or network diagnosis, Playwright test debugging, and browser-driven verification in this repository.
---
# Debug MyGO with Playwright CLI
Use the Playwright CLI bundled with the locked `@playwright/test` dependency. Keep every browser action behind the repository npm script so a future CLI entry-point change is isolated to `web/package.json`.
## Use the repository command contract
Run browser commands from `web/`:
```bash
mise exec -- npm run playwright:cli -- <command> [arguments]
```
The npm script changes to the repository root before starting Playwright. This makes the CLI discover `.playwright/cli.config.json` and keeps generated output under `.artifacts/playwright-cli/`.
Never invoke `playwright-cli`, `npx playwright-cli`, `npx playwright`, a global Playwright installation, or `@latest`. Do not install `@playwright/cli`; the locked stable `@playwright/test` package supplies the CLI.
## Prepare the local environment
Install locked JavaScript dependencies and the matching Chromium revision:
```bash
cd web
mise exec -- npm ci
mise exec -- npm run playwright:install:deps
mise exec -- npm run playwright:install
```
Treat the Debian system-library command as an environment setup step. Do not rerun it during ordinary debugging when the dependencies are already present.
For authenticated file workflows, start both services in separate long-running terminals:
```bash
# Repository root: backend API on 127.0.0.1:10086.
mise exec -- go run . serve
# web/: Vite on 127.0.0.1:5173, proxying /api to the backend.
mise exec -- npm run dev -- --host 127.0.0.1
```
Use existing local test credentials or credentials supplied for the task. Never write credentials into the Skill, source files, screenshots, or reports. Keep upload fixtures below 20 MB for the current milestone.
## Follow the debugging loop
1. Reproduce the problem with the narrowest existing test first. Confirm the test expresses the intended behavior before changing implementation code.
2. Confirm the required server is reachable. The login page itself only needs Vite; login, list, upload, and download also need the Go backend and suitable local data.
3. Open the page and read its accessibility snapshot:
```bash
mise exec -- npm run playwright:cli -- open http://127.0.0.1:5173/login
mise exec -- npm run playwright:cli -- snapshot
```
4. Use refs from the latest snapshot for interactions. Take another snapshot after navigation, modal changes, or substantial rerenders because refs can become stale.
5. Inspect browser and API evidence before guessing:
```bash
mise exec -- npm run playwright:cli -- console
mise exec -- npm run playwright:cli -- requests
mise exec -- npm run playwright:cli -- request <number>
```
6. Prefer snapshots for structure and screenshots for visual evidence:
```bash
mise exec -- npm run playwright:cli -- screenshot --filename=.artifacts/playwright-cli/mygo-debug.png
```
7. Close every session when finished, including error paths:
```bash
mise exec -- npm run playwright:cli -- close
mise exec -- npm run playwright:cli -- close-all
```
Use a named session with `-s=<name>` when parallel or persistent investigations would otherwise collide. Keep sessions isolated unless the task explicitly requires saved state.
## Debug Playwright tests
Run the repository suite from `web/`:
```bash
mise exec -- npm run test:e2e
```
For interactive debugging, start the test with `--debug=cli`, wait for its `tw-*` session name, then attach through the same npm wrapper. Read [playwright-tests.md](references/playwright-tests.md) before using this workflow.
## Load detailed references only when needed
- Read [element-attributes.md](references/element-attributes.md) when snapshots omit a required DOM attribute.
- Read [playwright-tests.md](references/playwright-tests.md) when attaching to a paused Playwright Test run.
- Read [request-mocking.md](references/request-mocking.md) when reproducing API failures with browser-level routes.
- Read [running-code.md](references/running-code.md) for multi-step page evaluation that individual CLI commands cannot express.
- Read [session-management.md](references/session-management.md) for named, persistent, attached, or parallel sessions.
- Read [spec-driven-testing.md](references/spec-driven-testing.md) when planning, generating, or healing E2E scenarios.
- Read [storage-state.md](references/storage-state.md) when inspecting or preserving cookies, local storage, or session storage.
- Read [test-generation.md](references/test-generation.md) when translating an interactive session into Playwright test code.
- Read [tracing.md](references/tracing.md) for trace capture and diagnosis.
- Read [video-recording.md](references/video-recording.md) only when a video materially improves the debugging result.
## Preserve repository boundaries
Write screenshots, traces, videos, downloaded responses, and temporary upload fixtures only under ignored `.artifacts/` paths. Do not change application data, create accounts, or call destructive UI actions unless the task authorizes those effects. Report browser launch or sandbox failures instead of silently disabling the configured Chromium sandbox.
@@ -0,0 +1,4 @@
interface:
display_name: "MyGO Playwright CLI Debugging"
short_description: "Headless browser debugging for MyGO Web"
default_prompt: "Use $playwright-cli to inspect and debug the MyGO Web interface in a headless browser."
@@ -0,0 +1,23 @@
# Inspecting Element Attributes
When the snapshot doesn't show an element's `id`, `class`, `data-*` attributes, or other DOM properties, use `eval` to inspect them.
## Examples
```bash
mise exec -- npm run playwright:cli -- snapshot
# snapshot shows a button as e7 but doesn't reveal its id or data attributes
# get the element's id
mise exec -- npm run playwright:cli -- eval "el => el.id" e7
# get all CSS classes
mise exec -- npm run playwright:cli -- eval "el => el.className" e7
# get a specific attribute
mise exec -- npm run playwright:cli -- eval "el => el.getAttribute('data-testid')" e7
mise exec -- npm run playwright:cli -- eval "el => el.getAttribute('aria-label')" e7
# get a computed style property
mise exec -- npm run playwright:cli -- eval "el => getComputedStyle(el).display" e7
```
@@ -0,0 +1,36 @@
# Running Playwright Tests
Run Playwright tests from `web/` through the repository script. To avoid opening the interactive HTML report, use the `PLAYWRIGHT_HTML_OPEN=never` environment variable.
```bash
# Run all tests
PLAYWRIGHT_HTML_OPEN=never mise exec -- npm run test:e2e --
```
# Debugging Playwright Tests
To debug a failing Playwright test, run it with `--debug=cli` option. This command will pause the test at the start and print the debugging instructions.
**IMPORTANT**: run the command in the background and check the output until "Debugging Instructions" is printed. Make sure to stop the command after you have finished.
Once instructions containing a session name are printed, use `mise exec -- npm run playwright:cli --` to attach the session and explore the page.
```bash
# Run the test
PLAYWRIGHT_HTML_OPEN=never mise exec -- npm run test:e2e -- --debug=cli
# ...
# ... debugging instructions for "tw-abcdef" session ...
# ...
# Attach to the test
mise exec -- npm run playwright:cli -- attach tw-abcdef
```
Keep the test running in the background while you explore and look for a fix.
The test is paused at the start, so you should step over or pause at a particular location
where the problem is most likely to be.
Every action you perform with `mise exec -- npm run playwright:cli --` generates corresponding Playwright TypeScript code.
This code appears in the output and can be copied directly into the test. Most of the time, a specific locator or an expectation should be updated, but it could also be a bug in the app. Use your judgement.
After fixing the test, stop the background test run. Rerun to check that test passes.
@@ -0,0 +1,87 @@
# Request Mocking
Intercept, mock, modify, and block network requests.
## CLI Route Commands
```bash
# Mock with custom status
mise exec -- npm run playwright:cli -- route "**/*.jpg" --status=404
# Mock with JSON body
mise exec -- npm run playwright:cli -- route "**/api/users" --body='[{"id":1,"name":"Alice"}]' --content-type=application/json
# Mock with custom headers
mise exec -- npm run playwright:cli -- route "**/api/data" --body='{"ok":true}' --header="X-Custom: value"
# Remove headers from requests
mise exec -- npm run playwright:cli -- route "**/*" --remove-header=cookie,authorization
# List active routes
mise exec -- npm run playwright:cli -- route-list
# Remove a route or all routes
mise exec -- npm run playwright:cli -- unroute "**/*.jpg"
mise exec -- npm run playwright:cli -- unroute
```
## URL Patterns
```
**/api/users - Exact path match
**/api/*/details - Wildcard in path
**/*.{png,jpg,jpeg} - Match file extensions
**/search?q=* - Match query parameters
```
## Advanced Mocking with run-code
For conditional responses, request body inspection, response modification, or delays:
### Conditional Response Based on Request
```bash
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.route('**/api/login', route => {
const body = route.request().postDataJSON();
if (body.username === 'admin') {
route.fulfill({ body: JSON.stringify({ token: 'mock-token' }) });
} else {
route.fulfill({ status: 401, body: JSON.stringify({ error: 'Invalid' }) });
}
});
}"
```
### Modify Real Response
```bash
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.route('**/api/user', async route => {
const response = await route.fetch();
const json = await response.json();
json.isPremium = true;
await route.fulfill({ response, json });
});
}"
```
### Simulate Network Failures
```bash
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.route('**/api/offline', route => route.abort('internetdisconnected'));
}"
# Options: connectionrefused, timedout, connectionreset, internetdisconnected
```
### Delayed Response
```bash
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.route('**/api/slow', async route => {
await new Promise(r => setTimeout(r, 3000));
route.fulfill({ body: JSON.stringify({ data: 'loaded' }) });
});
}"
```
@@ -0,0 +1,241 @@
# Running Custom Playwright Code
Use `run-code` to execute arbitrary Playwright code for advanced scenarios not covered by CLI commands.
## Syntax
```bash
mise exec -- npm run playwright:cli -- run-code "async page => {
// Your Playwright code here
// Access page.context() for browser context operations
}"
```
You can also load the function from a file:
```bash
mise exec -- npm run playwright:cli -- run-code --filename=./my-script.js
```
The code must be a single function expression, it is wrapped in `(...)` and evaluated.
import/export/require syntax is not supported.
## Geolocation
```bash
# Grant geolocation permission and set location
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.context().grantPermissions(['geolocation']);
await page.context().setGeolocation({ latitude: 37.7749, longitude: -122.4194 });
}"
# Set location to London
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.context().grantPermissions(['geolocation']);
await page.context().setGeolocation({ latitude: 51.5074, longitude: -0.1278 });
}"
# Clear geolocation override
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.context().clearPermissions();
}"
```
## Permissions
```bash
# Grant multiple permissions
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.context().grantPermissions([
'geolocation',
'notifications',
'camera',
'microphone'
]);
}"
# Grant permissions for specific origin
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.context().grantPermissions(['clipboard-read'], {
origin: 'https://example.com'
});
}"
```
## Media Emulation
```bash
# Emulate dark color scheme
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.emulateMedia({ colorScheme: 'dark' });
}"
# Emulate light color scheme
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.emulateMedia({ colorScheme: 'light' });
}"
# Emulate reduced motion
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.emulateMedia({ reducedMotion: 'reduce' });
}"
# Emulate print media
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.emulateMedia({ media: 'print' });
}"
```
## Wait Strategies
```bash
# Wait for network idle
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.waitForLoadState('networkidle');
}"
# Wait for specific element
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.locator('.loading').waitFor({ state: 'hidden' });
}"
# Wait for function to return true
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.waitForFunction(() => window.appReady === true);
}"
# Wait with timeout
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.locator('.result').waitFor({ timeout: 10000 });
}"
```
## Frames and Iframes
```bash
# Work with iframe
mise exec -- npm run playwright:cli -- run-code "async page => {
const frame = page.locator('iframe#my-iframe').contentFrame();
await frame.locator('button').click();
}"
# Get all frames
mise exec -- npm run playwright:cli -- run-code "async page => {
const frames = page.frames();
return frames.map(f => f.url());
}"
```
## File Downloads
```bash
# Handle file download
mise exec -- npm run playwright:cli -- run-code "async page => {
const downloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: 'Download' }).click();
const download = await downloadPromise;
await download.saveAs('./downloaded-file.pdf');
return download.suggestedFilename();
}"
```
## Clipboard
```bash
# Read clipboard (requires permission)
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.context().grantPermissions(['clipboard-read']);
return await page.evaluate(() => navigator.clipboard.readText());
}"
# Write to clipboard
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.evaluate(text => navigator.clipboard.writeText(text), 'Hello clipboard!');
}"
```
## Page Information
```bash
# Get page title
mise exec -- npm run playwright:cli -- run-code "async page => {
return await page.title();
}"
# Get current URL
mise exec -- npm run playwright:cli -- run-code "async page => {
return page.url();
}"
# Get page content
mise exec -- npm run playwright:cli -- run-code "async page => {
return await page.content();
}"
# Get viewport size
mise exec -- npm run playwright:cli -- run-code "async page => {
return page.viewportSize();
}"
```
## JavaScript Execution
```bash
# Execute JavaScript and return result
mise exec -- npm run playwright:cli -- run-code "async page => {
return await page.evaluate(() => {
return {
userAgent: navigator.userAgent,
language: navigator.language,
cookiesEnabled: navigator.cookieEnabled
};
});
}"
# Pass arguments to evaluate
mise exec -- npm run playwright:cli -- run-code "async page => {
const multiplier = 5;
return await page.evaluate(m => document.querySelectorAll('li').length * m, multiplier);
}"
```
## Error Handling
```bash
# Try-catch in run-code
mise exec -- npm run playwright:cli -- run-code "async page => {
try {
await page.getByRole('button', { name: 'Submit' }).click({ timeout: 1000 });
return 'clicked';
} catch (e) {
return 'element not found';
}
}"
```
## Complex Workflows
```bash
# Login and save state
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.goto('https://example.com/login');
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
await page.getByRole('textbox', { name: 'Password' }).fill('secret');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('**/dashboard');
await page.context().storageState({ path: 'auth.json' });
return 'Login successful';
}"
# Scrape data from multiple pages
mise exec -- npm run playwright:cli -- run-code "async page => {
const results = [];
for (let i = 1; i <= 3; i++) {
await page.goto(\`https://example.com/page/\${i}\`);
const items = await page.locator('.item').allTextContents();
results.push(...items);
}
return results;
}"
```
@@ -0,0 +1,225 @@
# Browser Session Management
Run multiple isolated browser sessions concurrently with state persistence.
## Named Browser Sessions
Use `-s` flag to isolate browser contexts:
```bash
# Browser 1: Authentication flow
mise exec -- npm run playwright:cli -- -s=auth open https://app.example.com/login
# Browser 2: Public browsing (separate cookies, storage)
mise exec -- npm run playwright:cli -- -s=public open https://example.com
# Commands are isolated by browser session
mise exec -- npm run playwright:cli -- -s=auth fill e1 "user@example.com"
mise exec -- npm run playwright:cli -- -s=public snapshot
```
## Browser Session Isolation Properties
Each browser session has independent:
- Cookies
- LocalStorage / SessionStorage
- IndexedDB
- Cache
- Browsing history
- Open tabs
## Browser Session Commands
```bash
# List all browser sessions
mise exec -- npm run playwright:cli -- list
# Stop a browser session (close the browser)
mise exec -- npm run playwright:cli -- close # stop the default browser
mise exec -- npm run playwright:cli -- -s=mysession close # stop a named browser
# Stop all browser sessions
mise exec -- npm run playwright:cli -- close-all
# Forcefully kill all daemon processes (for stale/zombie processes)
mise exec -- npm run playwright:cli -- kill-all
# Delete browser session user data (profile directory)
mise exec -- npm run playwright:cli -- delete-data # delete default browser data
mise exec -- npm run playwright:cli -- -s=mysession delete-data # delete named browser data
```
## Environment Variable
Set a default browser session name via environment variable:
```bash
export PLAYWRIGHT_CLI_SESSION="mysession"
mise exec -- npm run playwright:cli -- open example.com # Uses "mysession" automatically
```
## Common Patterns
### Concurrent Scraping
```bash
#!/bin/bash
# Scrape multiple sites concurrently
# Start all browsers
mise exec -- npm run playwright:cli -- -s=site1 open https://site1.com &
mise exec -- npm run playwright:cli -- -s=site2 open https://site2.com &
mise exec -- npm run playwright:cli -- -s=site3 open https://site3.com &
wait
# Take snapshots from each
mise exec -- npm run playwright:cli -- -s=site1 snapshot
mise exec -- npm run playwright:cli -- -s=site2 snapshot
mise exec -- npm run playwright:cli -- -s=site3 snapshot
# Cleanup
mise exec -- npm run playwright:cli -- close-all
```
### A/B Testing Sessions
```bash
# Test different user experiences
mise exec -- npm run playwright:cli -- -s=variant-a open "https://app.com?variant=a"
mise exec -- npm run playwright:cli -- -s=variant-b open "https://app.com?variant=b"
# Compare
mise exec -- npm run playwright:cli -- -s=variant-a screenshot
mise exec -- npm run playwright:cli -- -s=variant-b screenshot
```
### Persistent Profile
By default, browser profile is kept in memory only. Use `--persistent` flag on `open` to persist the browser profile to disk:
```bash
# Use persistent profile (auto-generated location)
mise exec -- npm run playwright:cli -- open https://example.com --persistent
# Use persistent profile with custom directory
mise exec -- npm run playwright:cli -- open https://example.com --profile=/path/to/profile
```
## Attaching to a Running Browser
Use `attach` to connect to a browser that is already running, instead of launching a new one.
### Attach by channel name
Connect to a running Chrome or Edge instance by its channel name. The browser must have remote debugging enabled — navigate to `chrome://inspect/#remote-debugging` in the target browser and check "Allow remote debugging for this browser instance".
```bash
# Attach to Chrome
mise exec -- npm run playwright:cli -- attach --cdp=chrome
# Attach to Chrome Canary
mise exec -- npm run playwright:cli -- attach --cdp=chrome-canary
# Attach to Microsoft Edge
mise exec -- npm run playwright:cli -- attach --cdp=msedge
# Attach to Edge Dev
mise exec -- npm run playwright:cli -- attach --cdp=msedge-dev
```
Supported channels: `chrome`, `chrome-beta`, `chrome-dev`, `chrome-canary`, `msedge`, `msedge-beta`, `msedge-dev`, `msedge-canary`.
When `--session` is not provided, the session is named after the channel (e.g. `--cdp=msedge` creates a session called `msedge`), so parallel attaches to Chrome and Edge don't collide on `default`. Pass `--session=<name>` to override.
### Attach via CDP endpoint
Connect to a browser that exposes a Chrome DevTools Protocol endpoint:
```bash
mise exec -- npm run playwright:cli -- attach --cdp=http://localhost:9222
```
### Attach via browser extension
Connect to a browser with the Playwright extension installed:
```bash
mise exec -- npm run playwright:cli -- attach --extension
```
### Detach
Tear down an attached session without affecting the external browser:
```bash
# Detach the default attached session
mise exec -- npm run playwright:cli -- detach
# Detach a specific attached session
mise exec -- npm run playwright:cli -- -s=msedge detach
```
`detach` only works on sessions created via `attach`. For sessions created via `open`, use `close`.
## Default Browser Session
When `-s` is omitted, commands use the default browser session:
```bash
# These use the same default browser session
mise exec -- npm run playwright:cli -- open https://example.com
mise exec -- npm run playwright:cli -- snapshot
mise exec -- npm run playwright:cli -- close # Stops default browser
```
## Browser Session Configuration
Configure a browser session with specific settings when opening:
```bash
# Open with config file
mise exec -- npm run playwright:cli -- open https://example.com --config=.playwright/my-cli.json
# Open with specific browser
mise exec -- npm run playwright:cli -- open https://example.com --browser=firefox
# Open in headed mode
mise exec -- npm run playwright:cli -- open https://example.com --headed
# Open with persistent profile
mise exec -- npm run playwright:cli -- open https://example.com --persistent
```
## Best Practices
### 1. Name Browser Sessions Semantically
```bash
# GOOD: Clear purpose
mise exec -- npm run playwright:cli -- -s=github-auth open https://github.com
mise exec -- npm run playwright:cli -- -s=docs-scrape open https://docs.example.com
# AVOID: Generic names
mise exec -- npm run playwright:cli -- -s=s1 open https://github.com
```
### 2. Always Clean Up
```bash
# Stop browsers when done
mise exec -- npm run playwright:cli -- -s=auth close
mise exec -- npm run playwright:cli -- -s=scrape close
# Or stop all at once
mise exec -- npm run playwright:cli -- close-all
# If browsers become unresponsive or zombie processes remain
mise exec -- npm run playwright:cli -- kill-all
```
### 3. Delete Stale Browser Data
```bash
# Remove old browser data to free disk space
mise exec -- npm run playwright:cli -- -s=oldsession delete-data
```
@@ -0,0 +1,305 @@
# Spec-driven testing (plan → generate → heal)
End-to-end workflow for authoring and maintaining Playwright tests using `mise exec -- npm run playwright:cli --`. The three sections below can be used independently:
- **Planning** — explore the app, produce a spec file describing what to test.
- **Generate** — turn a spec into Playwright test files. Update the spec if it's vague or stale.
- **Heal** — diagnose failing tests, fix the code, reconcile the spec with reality.
All three lean on the same mechanic: run `mise exec -- npm run test:e2e -- --debug=cli` in the background, then `mise exec -- npm run playwright:cli -- attach tw-XXXX` to drive the paused page interactively. See [playwright-tests.md](playwright-tests.md) for the debug/attach mechanics and [test-generation.md](test-generation.md) for how every `mise exec -- npm run playwright:cli --` action emits Playwright TypeScript.
---
## 1. Planning
Goal: produce a spec file (e.g. `specs/<feature>.plan.md`) that enumerates the scenarios to test. **Always** write the spec to a file.
### 1.1 Prerequisite: workspace
Check the workspace has Playwright installed before anything else:
```bash
# These confirm the MyGO Web workspace and locked CLI:
test -f playwright.config.ts
mise exec -- npm run playwright:cli -- --version
```
If dependencies are missing, restore the repository lockfile instead of bootstrapping a new Playwright project:
```bash
mise exec -- npm ci
```
### 1.2 Prerequisite: seed test
A **seed test** is a minimal test that lands the page in the state every scenario starts from: navigation to the app, any required login, feature flags, etc. Scenarios assume a fresh start *after* the seed. `--debug=cli` pauses *inside* this test, so the seed is where every planning and generation session begins.
Minimum viable seed:
```ts
// e2e/seed.spec.ts
import { test } from '@playwright/test';
test('seed', async ({ page }) => {
await page.goto('https://example.com/');
});
```
Preferred — push navigation into a fixture so scenario tests reuse it:
```ts
// e2e/fixtures.ts
import { test as baseTest } from '@playwright/test';
export { expect } from '@playwright/test';
export const test = baseTest.extend({
page: async ({ page }, use) => {
await page.goto('https://example.com/');
await use(page);
},
});
```
```ts
// e2e/seed.spec.ts
import { test } from './fixtures';
test('seed', async ({ page }) => {
// Fixture already navigates. This empty body tells agents where to start.
});
```
If no seed exists, create one that at least navigates to the app.
### 1.3 Explore the app
Launch the app via the seed in the background and attach:
```bash
PLAYWRIGHT_HTML_OPEN=never mise exec -- npm run test:e2e -- e2e/seed.spec.ts --debug=cli
# wait for "Debugging Instructions" and the session name tw-XXXX
mise exec -- npm run playwright:cli -- attach tw-XXXX
```
Resume so the seed runs, then probe the app:
```bash
mise exec -- npm run playwright:cli -- resume # resume so that seed test runs fully
mise exec -- npm run playwright:cli -- snapshot # inventory of interactive elements
mise exec -- npm run playwright:cli -- click e5 # follow a flow
mise exec -- npm run playwright:cli -- eval "location.href" # read URL / state
mise exec -- npm run playwright:cli -- show --annotate # ask the user to point at something
```
Map out:
- Interactive surfaces (forms, buttons, lists, filters, modals).
- Primary user journeys end-to-end.
- Edge cases: empty states, validation errors, very long input, boundary values.
- Persistence: reload, local/session storage, URL fragments.
- Navigation: which controls change the URL, back/forward behaviour.
**Important**: Do not just open the app url with mise exec -- npm run playwright:cli --, always go through the test to capture any custom setup done there.
**Important**: Stop the background test when done exploring.
### 1.4 Write the spec file
Save under `specs/<feature>.plan.md`. Use this structure:
```markdown
# <Feature> Test Plan
## Application Overview
<One paragraph describing what the feature does and why it matters.>
## Test Scenarios
### 1. <Group Name>
**Seed:** `e2e/seed.spec.ts`
#### 1.1. <kebab-case-scenario-name>
**File:** `e2e/<group>/<kebab-case-scenario-name>.spec.ts`
**Steps:**
1. <Concrete user step>
- expect: <observable outcome>
- expect: <another observable outcome>
2. <Next step>
- expect: <outcome>
#### 1.2. <next-scenario>
...
### 2. <Next Group>
**Seed:** `e2e/seed.spec.ts`
...
```
Guidelines:
- Each scenario is independent and starts from the seed's fresh state — never chain scenarios.
- Scenario names are kebab-case and match the test file name (`should-add-single-todo``should-add-single-todo.spec.ts`).
- Cover happy path, edge cases, validation, negative flows, persistence.
- Write steps at the user level ("Type 'Buy milk' into the input"), not the API level ("call `fill`").
- Put observable outcomes in `- expect:` bullets; each becomes an assertion during generation.
---
## 2. Generate
Goal: take a spec file and produce Playwright test files. Optionally update the spec if it has drifted.
### 2.1 Inputs
- **Spec file**, e.g. `specs/basic-operations.plan.md`.
- **Target**: either a single scenario (e.g. `1.2`), a whole group (`1`), or all.
- **Seed file**, read from the `**Seed:**` line of the scenario's group.
### 2.2 Generate one scenario
For each target scenario, in sequence (never in parallel — scenarios share the seed session):
```bash
PLAYWRIGHT_HTML_OPEN=never mise exec -- npm run test:e2e -- <seed-file> --debug=cli # background
mise exec -- npm run playwright:cli -- attach tw-XXXX
# resume
```
**Do not** just open the app url with mise exec -- npm run playwright:cli --, always go through the test to capture any custom setup done there.
Walk the scenario's `Steps:` one by one with `mise exec -- npm run playwright:cli --`, treating the spec as the plan and the live app as the source of truth. If a step is vague ("click the button" — which button?), references an element that no longer exists, or contradicts the app's actual behaviour, use your judgement: update the spec to match what the app really does, then keep going. Editing the spec mid-generation is expected.
Every action prints the equivalent Playwright TypeScript (see [test-generation.md](test-generation.md)):
```bash
mise exec -- npm run playwright:cli -- snapshot # find refs
mise exec -- npm run playwright:cli -- fill e3 "John Doe" # -> page.getByRole('textbox', {...}).fill(...)
mise exec -- npm run playwright:cli -- press Enter
mise exec -- npm run playwright:cli -- click e7
```
For each `- expect:` bullet, add an explicit assertion. See [test-generation.md](test-generation.md) for details.
Collect the generated code and write the test file at the path given in the spec:
```ts
// spec: specs/basic-operations.plan.md
// seed: e2e/seed.spec.ts
import { test, expect } from './fixtures'; // or '@playwright/test' if no fixtures file
test.describe('Signing in and out', () => {
test('should sign in', async ({ page }) => {
// 1. Navigate to the application
// (handled by the seed fixture)
// 2. Type 'John Doe' into the username field
await page.getByRole('textbox', { name: 'username' }).fill('John Doe');
// 3. Type password
await page.getByRole('textbox', { name: 'password' }).fill('TestPassword');
// 4. Press Enter to submit
await page.getByRole('textbox', { name: 'password' }).press('Enter');
await expect(page.getByRole('heading')).toContainText('Welcome, John Doe!');
});
});
```
Rules:
- **One test per file.** File path, describe name, and test name come verbatim from the spec (minus the ordinal).
- Prefix each numbered step with a `// N. <step text>` comment before its actions.
- Use the describe group name verbatim from the spec (no `1.` ordinal).
- Import from `./fixtures` if the project has one; otherwise `@playwright/test`.
- **Important**: close the CLI session and stop the background test before moving to the next scenario.
### 2.3 Generate multiple scenarios
Loop 2.2 over the targeted scenarios one at a time, restarting the seed between each so every test starts from a clean page. This is safe to parallelise due to unique generated session names - just make sure each test run is stopped.
### 2.4 Run generated tests
After generation, run the new tests once:
```bash
PLAYWRIGHT_HTML_OPEN=never mise exec -- npm run test:e2e -- e2e/<group>/<scenario>.spec.ts
```
Any failure goes to Section 3.
---
## 3. Heal
Goal: fix failing tests, and update the spec if the app's intended behaviour changed.
### 3.1 Find failing tests
```bash
PLAYWRIGHT_HTML_OPEN=never mise exec -- npm run test:e2e --
```
Record the list of failing `<file>:<line>` entries and process them one at a time. Do not attempt parallel fixes — shared state and the single CLI session make that fragile.
### 3.2 Debug one failure
Run the single failing test in debug mode in the background, then attach:
```bash
PLAYWRIGHT_HTML_OPEN=never mise exec -- npm run test:e2e -- e2e/<group>/<scenario>.spec.ts:<line> --debug=cli
# wait for "Debugging Instructions" and the tw-XXXX session name
mise exec -- npm run playwright:cli -- attach tw-XXXX
```
The test is paused at the start. Step forward or run to until just before the failing action or assertion, then diagnose:
```bash
mise exec -- npm run playwright:cli -- snapshot # did the element change / move / rename?
mise exec -- npm run playwright:cli -- console # app-side errors?
mise exec -- npm run playwright:cli -- requests # failed request? wrong payload?
mise exec -- npm run playwright:cli -- show --annotate # ask the user to point somewhere
```
Common causes: selector drift, new wrapper element, label/ARIA rename, timing (transition, async load), assertion text updated in the app, test data leaking between runs.
Rehearse the corrected interaction with `mise exec -- npm run playwright:cli --` — the generated code in the output is what you paste back into the test.
### 3.3 Apply the fix
Edit the test file: update the locator, assertion, step order, or inputs to match the corrected behaviour. Stop the background debug run. Rerun the single test to confirm green.
Never skip hooks or add sleeps as a fix. Never use `networkidle`.
### 3.4 Reconcile with the spec
Open the spec referenced by the `// spec:` header in the test file and locate the scenario that matches the test.
- **Fix was purely technical** (locator drift, better assertion shape) and the spec's user-level behaviour still matches the app → leave the spec alone.
- **Fix changed user-visible steps, inputs, order, or expected outcomes** that the spec describes → update the spec to match reality. Keep the scenario id and file path stable; only the step / expect lines change.
- **Unclear whether the app change is intentional** (spec is stale) **or a regression** (test was right, app is wrong) → **stop and ask the user**. Provide:
- the scenario id (e.g. `2.3`),
- the spec lines that no longer match,
- the observed app behaviour (quote a snapshot excerpt or a concrete outcome).
Only after the user answers, either update the spec (intentional change) or file/flag the test as covering a bug (regression).
### 3.5 Iteration and giving up
- Fix failures one at a time; rerun after each.
- If after thorough investigation you are confident the test is correct but the app is wrong *and* the user has confirmed it's a bug: mark the test `test.fixme(...)` with a comment pointing at the user's decision or issue link. Never silently skip.
---
## Cross-references
| For... | See |
|---|---|
| `--debug=cli` / attach mechanics | [playwright-tests.md](playwright-tests.md) |
| How `mise exec -- npm run playwright:cli --` actions become TS | [test-generation.md](test-generation.md) |
| Mocking requests during exploration/generation | [request-mocking.md](request-mocking.md) |
| Managing the CLI browser session | [session-management.md](session-management.md) |
@@ -0,0 +1,275 @@
# Storage Management
Manage cookies, localStorage, sessionStorage, and browser storage state.
## Storage State
Save and restore complete browser state including cookies and storage.
### Save Storage State
```bash
# Save to auto-generated filename (storage-state-{timestamp}.json)
mise exec -- npm run playwright:cli -- state-save
# Save to specific filename
mise exec -- npm run playwright:cli -- state-save .artifacts/playwright-cli/my-auth-state.json
```
### Restore Storage State
```bash
# Load storage state from file
mise exec -- npm run playwright:cli -- state-load .artifacts/playwright-cli/my-auth-state.json
# Reload page to apply cookies
mise exec -- npm run playwright:cli -- open https://example.com
```
### Storage State File Format
The saved file contains:
```json
{
"cookies": [
{
"name": "session_id",
"value": "abc123",
"domain": "example.com",
"path": "/",
"expires": 1893456000,
"httpOnly": true,
"secure": true,
"sameSite": "Lax"
}
],
"origins": [
{
"origin": "https://example.com",
"localStorage": [
{ "name": "theme", "value": "dark" },
{ "name": "user_id", "value": "12345" }
]
}
]
}
```
## Cookies
### List All Cookies
```bash
mise exec -- npm run playwright:cli -- cookie-list
```
### Filter Cookies by Domain
```bash
mise exec -- npm run playwright:cli -- cookie-list --domain=example.com
```
### Filter Cookies by Path
```bash
mise exec -- npm run playwright:cli -- cookie-list --path=/api
```
### Get Specific Cookie
```bash
mise exec -- npm run playwright:cli -- cookie-get session_id
```
### Set a Cookie
```bash
# Basic cookie
mise exec -- npm run playwright:cli -- cookie-set session abc123
# Cookie with options
mise exec -- npm run playwright:cli -- cookie-set session abc123 --domain=example.com --path=/ --httpOnly --secure --sameSite=Lax
# Cookie with expiration (Unix timestamp)
mise exec -- npm run playwright:cli -- cookie-set remember_me token123 --expires=1893456000
```
### Delete a Cookie
```bash
mise exec -- npm run playwright:cli -- cookie-delete session_id
```
### Clear All Cookies
```bash
mise exec -- npm run playwright:cli -- cookie-clear
```
### Advanced: Multiple Cookies or Custom Options
For complex scenarios like adding multiple cookies at once, use `run-code`:
```bash
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.context().addCookies([
{ name: 'session_id', value: 'sess_abc123', domain: 'example.com', path: '/', httpOnly: true },
{ name: 'preferences', value: JSON.stringify({ theme: 'dark' }), domain: 'example.com', path: '/' }
]);
}"
```
## Local Storage
### List All localStorage Items
```bash
mise exec -- npm run playwright:cli -- localstorage-list
```
### Get Single Value
```bash
mise exec -- npm run playwright:cli -- localstorage-get token
```
### Set Value
```bash
mise exec -- npm run playwright:cli -- localstorage-set theme dark
```
### Set JSON Value
```bash
mise exec -- npm run playwright:cli -- localstorage-set user_settings '{"theme":"dark","language":"en"}'
```
### Delete Single Item
```bash
mise exec -- npm run playwright:cli -- localstorage-delete token
```
### Clear All localStorage
```bash
mise exec -- npm run playwright:cli -- localstorage-clear
```
### Advanced: Multiple Operations
For complex scenarios like setting multiple values at once, use `run-code`:
```bash
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.evaluate(() => {
localStorage.setItem('token', 'jwt_abc123');
localStorage.setItem('user_id', '12345');
localStorage.setItem('expires_at', Date.now() + 3600000);
});
}"
```
## Session Storage
### List All sessionStorage Items
```bash
mise exec -- npm run playwright:cli -- sessionstorage-list
```
### Get Single Value
```bash
mise exec -- npm run playwright:cli -- sessionstorage-get form_data
```
### Set Value
```bash
mise exec -- npm run playwright:cli -- sessionstorage-set step 3
```
### Delete Single Item
```bash
mise exec -- npm run playwright:cli -- sessionstorage-delete step
```
### Clear sessionStorage
```bash
mise exec -- npm run playwright:cli -- sessionstorage-clear
```
## IndexedDB
### List Databases
```bash
mise exec -- npm run playwright:cli -- run-code "async page => {
return await page.evaluate(async () => {
const databases = await indexedDB.databases();
return databases;
});
}"
```
### Delete Database
```bash
mise exec -- npm run playwright:cli -- run-code "async page => {
await page.evaluate(() => {
indexedDB.deleteDatabase('myDatabase');
});
}"
```
## Common Patterns
### Authentication State Reuse
```bash
# Step 1: Login and save state
mise exec -- npm run playwright:cli -- open https://app.example.com/login
mise exec -- npm run playwright:cli -- snapshot
mise exec -- npm run playwright:cli -- fill e1 "user@example.com"
mise exec -- npm run playwright:cli -- fill e2 "password123"
mise exec -- npm run playwright:cli -- click e3
# Save the authenticated state
mise exec -- npm run playwright:cli -- state-save .artifacts/playwright-cli/auth.json
# Step 2: Later, restore state and skip login
mise exec -- npm run playwright:cli -- state-load .artifacts/playwright-cli/auth.json
mise exec -- npm run playwright:cli -- open https://app.example.com/dashboard
# Already logged in!
```
### Save and Restore Roundtrip
```bash
# Set up authentication state
mise exec -- npm run playwright:cli -- open https://example.com
mise exec -- npm run playwright:cli -- eval "() => { document.cookie = 'session=abc123'; localStorage.setItem('user', 'john'); }"
# Save state to file
mise exec -- npm run playwright:cli -- state-save .artifacts/playwright-cli/my-session.json
# ... later, in a new session ...
# Restore state
mise exec -- npm run playwright:cli -- state-load .artifacts/playwright-cli/my-session.json
mise exec -- npm run playwright:cli -- open https://example.com
# Cookies and localStorage are restored!
```
## Security Notes
- Never commit storage state files containing auth tokens
- Add `*.auth-state.json` to `.gitignore`
- Delete state files after automation completes
- Use environment variables for sensitive data
- By default, sessions run in-memory mode which is safer for sensitive operations
@@ -0,0 +1,134 @@
# Test Generation
Generate Playwright test code automatically as you interact with the browser.
## How It Works
Every action you perform with `mise exec -- npm run playwright:cli --` generates corresponding Playwright TypeScript code.
This code appears in the output and can be copied directly into your test files.
## Example Workflow
```bash
# Start a session
mise exec -- npm run playwright:cli -- open https://example.com/login
# Take a snapshot to see elements
mise exec -- npm run playwright:cli -- snapshot
# Output shows: e1 [textbox "Email"], e2 [textbox "Password"], e3 [button "Sign In"]
# Fill form fields - generates code automatically
mise exec -- npm run playwright:cli -- fill e1 "user@example.com"
# Ran Playwright code:
# await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
mise exec -- npm run playwright:cli -- fill e2 "password123"
# Ran Playwright code:
# await page.getByRole('textbox', { name: 'Password' }).fill('password123');
mise exec -- npm run playwright:cli -- click e3
# Ran Playwright code:
# await page.getByRole('button', { name: 'Sign In' }).click();
```
## Building a Test File
Collect the generated code into a Playwright test:
```typescript
import { test, expect } from '@playwright/test';
test('login flow', async ({ page }) => {
// Generated code from mise exec -- npm run playwright:cli -- session:
await page.goto('https://example.com/login');
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
await page.getByRole('textbox', { name: 'Password' }).fill('password123');
await page.getByRole('button', { name: 'Sign In' }).click();
// Add assertions
await expect(page).toHaveURL(/.*dashboard/);
});
```
## Best Practices
### 1. Use Semantic Locators
The generated code uses role-based locators when possible, which are more resilient:
```typescript
// Generated (good - semantic)
await page.getByRole('button', { name: 'Submit' }).click();
// Avoid (fragile - CSS selectors)
await page.locator('#submit-btn').click();
```
### 2. Explore Before Recording
Take snapshots to understand the page structure before recording actions:
```bash
mise exec -- npm run playwright:cli -- open https://example.com
mise exec -- npm run playwright:cli -- snapshot
# Review the element structure
mise exec -- npm run playwright:cli -- click e5
```
### 3. Add Assertions Manually
Generated code captures actions but not assertions. Add expectations in your test using one of the recommended matchers:
- `toBeVisible()` — element is rendered and visible
- `toHaveText(text)` — element text content matches
- `toHaveValue(value) / toBeEmpty()` — input/select value matches
- `toBeChecked() / toBeUnchecked()` — checkbox state matches
- `toMatchAriaSnapshot(snapshot)` — page (or locator) matches a partial accessibility snapshot
Use `mise exec -- npm run playwright:cli -- generate-locator <target>` to produce the locator expression for the assertion, and the snapshot/eval commands to capture the expected value.
When asserting text content, make sure that generated locator does not contain text from the element itself. `getByTestId()` or `getByLabel()` usually work well with asserting text. When locator is text-based, prefer `toBeVisible()` instead.
Snapshot to be matched does not have to contain all the information - only capture what's necessary for the assertion. You can use regular expressions for unstable values.
```bash
# Get a stable locator for an element ref to use in the assertion
mise exec -- npm run playwright:cli -- --raw generate-locator e5
# getByRole('button', { name: 'Submit' })
# Capture expected text content for toHaveText
mise exec -- npm run playwright:cli -- --raw eval "el => el.textContent" e5
# Capture expected input value for toHaveValue/toBeEmpty
mise exec -- npm run playwright:cli -- --raw eval "el => el.value" e5
# Capture expected aria snapshot for toMatchAriaSnapshot/toBeChecked
# (whole page, or use a ref to scope to a region)
mise exec -- npm run playwright:cli -- --raw snapshot
mise exec -- npm run playwright:cli -- --raw snapshot e5
```
```typescript
// Generated action
await page.getByRole('button', { name: 'Submit' }).click();
// Manual assertions using the outputs above:
await expect(page.getByRole('alert', { name: 'Success' })).toBeVisible();
await expect(page.getByTestId('main-header')).toHaveText('Welcome, user');
await expect(page.getByRole('textbox', { name: 'Email' })).toHaveValue('user@example.com');
await expect(page.getByRole('checkbox', { name: 'Enable notifications' })).toBeChecked();
// toMatchAriaSnapshot on the whole page, finds a matching region
await expect(page).toMatchAriaSnapshot(`
- heading "Welcome, user"
- link /\\d+ new messages?/
- button "Sign out"
`);
// toMatchAriaSnapshot scoped to a region
await expect(page.getByRole('navigation')).toMatchAriaSnapshot(`
- link "Home"
- link /\\d+ new messages?/
- link "Profile"
`);
```
@@ -0,0 +1,139 @@
# Tracing
Capture detailed execution traces for debugging and analysis. Traces include DOM snapshots, screenshots, network activity, and console logs.
## Basic Usage
```bash
# Start trace recording
mise exec -- npm run playwright:cli -- tracing-start
# Perform actions
mise exec -- npm run playwright:cli -- open https://example.com
mise exec -- npm run playwright:cli -- click e1
mise exec -- npm run playwright:cli -- fill e2 "test"
# Stop trace recording
mise exec -- npm run playwright:cli -- tracing-stop
```
## Trace Output Files
When you start tracing, Playwright creates a `traces/` directory with several files:
### `trace-{timestamp}.trace`
**Action log** - The main trace file containing:
- Every action performed (clicks, fills, navigations)
- DOM snapshots before and after each action
- Screenshots at each step
- Timing information
- Console messages
- Source locations
### `trace-{timestamp}.network`
**Network log** - Complete network activity:
- All HTTP requests and responses
- Request headers and bodies
- Response headers and bodies
- Timing (DNS, connect, TLS, TTFB, download)
- Resource sizes
- Failed requests and errors
### `resources/`
**Resources directory** - Cached resources:
- Images, fonts, stylesheets, scripts
- Response bodies for replay
- Assets needed to reconstruct page state
## What Traces Capture
| Category | Details |
|----------|---------|
| **Actions** | Clicks, fills, hovers, keyboard input, navigations |
| **DOM** | Full DOM snapshot before/after each action |
| **Screenshots** | Visual state at each step |
| **Network** | All requests, responses, headers, bodies, timing |
| **Console** | All console.log, warn, error messages |
| **Timing** | Precise timing for each operation |
## Use Cases
### Debugging Failed Actions
```bash
mise exec -- npm run playwright:cli -- tracing-start
mise exec -- npm run playwright:cli -- open https://app.example.com
# This click fails - why?
mise exec -- npm run playwright:cli -- click e5
mise exec -- npm run playwright:cli -- tracing-stop
# Open trace to see DOM state when click was attempted
```
### Analyzing Performance
```bash
mise exec -- npm run playwright:cli -- tracing-start
mise exec -- npm run playwright:cli -- open https://slow-site.com
mise exec -- npm run playwright:cli -- tracing-stop
# View network waterfall to identify slow resources
```
### Capturing Evidence
```bash
# Record a complete user flow for documentation
mise exec -- npm run playwright:cli -- tracing-start
mise exec -- npm run playwright:cli -- open https://app.example.com/checkout
mise exec -- npm run playwright:cli -- fill e1 "4111111111111111"
mise exec -- npm run playwright:cli -- fill e2 "12/25"
mise exec -- npm run playwright:cli -- fill e3 "123"
mise exec -- npm run playwright:cli -- click e4
mise exec -- npm run playwright:cli -- tracing-stop
# Trace shows exact sequence of events
```
## Trace vs Video vs Screenshot
| Feature | Trace | Video | Screenshot |
|---------|-------|-------|------------|
| **Format** | .trace file | .webm video | .png/.jpeg image |
| **DOM inspection** | Yes | No | No |
| **Network details** | Yes | No | No |
| **Step-by-step replay** | Yes | Continuous | Single frame |
| **File size** | Medium | Large | Small |
| **Best for** | Debugging | Demos | Quick capture |
## Best Practices
### 1. Start Tracing Before the Problem
```bash
# Trace the entire flow, not just the failing step
mise exec -- npm run playwright:cli -- tracing-start
mise exec -- npm run playwright:cli -- open https://example.com
# ... all steps leading to the issue ...
mise exec -- npm run playwright:cli -- tracing-stop
```
### 2. Clean Up Old Traces
Traces can consume significant disk space:
```bash
# Remove traces older than 7 days
find ../.artifacts/playwright-cli -type f -mtime +7 -delete
```
## Limitations
- Traces add overhead to automation
- Large traces can consume significant disk space
- Some dynamic content may not replay perfectly
@@ -0,0 +1,143 @@
# Video Recording
Capture browser automation sessions as video for debugging, documentation, or verification. Produces WebM (VP8/VP9 codec).
## Basic Recording
```bash
# Open browser first
mise exec -- npm run playwright:cli -- open
# Start recording
mise exec -- npm run playwright:cli -- video-start .artifacts/playwright-cli/demo.webm
# Add a chapter marker for section transitions
mise exec -- npm run playwright:cli -- video-chapter "Getting Started" --description="Opening the homepage" --duration=2000
# Navigate and perform actions
mise exec -- npm run playwright:cli -- goto https://example.com
mise exec -- npm run playwright:cli -- snapshot
mise exec -- npm run playwright:cli -- click e1
# Add another chapter
mise exec -- npm run playwright:cli -- video-chapter "Filling Form" --description="Entering test data" --duration=2000
mise exec -- npm run playwright:cli -- fill e2 "test input"
# Stop and save
mise exec -- npm run playwright:cli -- video-stop
```
## Best Practices
### 1. Use Descriptive Filenames
```bash
# Include context in filename
mise exec -- npm run playwright:cli -- video-start .artifacts/playwright-cli/recordings/login-flow-2024-01-15.webm
mise exec -- npm run playwright:cli -- video-start .artifacts/playwright-cli/recordings/checkout-test-run-42.webm
```
### 2. Record entire hero scripts.
When recording a video for the user or as a proof of work, it is best to create a code snippet and execute it with run-code.
It allows inserting appropriate pauses between the actions and annotating the video. There are new Playwright APIs for that.
1) Perform scenario using CLI and take note of all locators and actions. You'll need those locators to request their bounding boxes for highlight.
2) Create a file with the intended script for video (below). Use pressSequentially w/ delay for nice typing, make reasonable pauses.
3) Use mise exec -- npm run playwright:cli -- run-code --filename your-script.js
**Important**: Overlays are `pointer-events: none` — they do not interfere with page interactions. You can safely keep sticky overlays visible while clicking, filling, or performing any actions on the page.
```js
async page => {
await page.screencast.start({ path: 'video.webm', size: { width: 1280, height: 800 } });
await page.goto('https://demo.playwright.dev/todomvc');
// Show a chapter card — blurs the page and shows a dialog.
// Blocks until duration expires, then auto-removes.
// Use this for simple use cases, but always feel free to hand-craft your own beautiful
// overlay via await page.screencast.showOverlay().
await page.screencast.showChapter('Adding Todo Items', {
description: 'We will add several items to the todo list.',
duration: 2000,
});
// Perform action
await page.getByRole('textbox', { name: 'What needs to be done?' }).pressSequentially('Walk the dog', { delay: 60 });
await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter');
await page.waitForTimeout(1000);
// Show next chapter
await page.screencast.showChapter('Verifying Results', {
description: 'Checking the item appeared in the list.',
duration: 2000,
});
// Add a sticky annotation that stays while you perform actions.
// Overlays are pointer-events: none, so they won't block clicks.
const annotation = await page.screencast.showOverlay(`
<div style="position: absolute; top: 8px; right: 8px;
padding: 6px 12px; background: rgba(0,0,0,0.7);
border-radius: 8px; font-size: 13px; color: white;">
✓ Item added successfully
</div>
`);
// Perform more actions while the annotation is visible
await page.getByRole('textbox', { name: 'What needs to be done?' }).pressSequentially('Buy groceries', { delay: 60 });
await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter');
await page.waitForTimeout(1500);
// Remove the annotation when done
await annotation.dispose();
// You can also highlight relevant locators and provide contextual annotations.
const bounds = await page.getByText('Walk the dog').boundingBox();
await page.screencast.showOverlay(`
<div style="position: absolute;
top: ${bounds.y}px;
left: ${bounds.x}px;
width: ${bounds.width}px;
height: ${bounds.height}px;
border: 1px solid red;">
</div>
<div style="position: absolute;
top: ${bounds.y + bounds.height + 5}px;
left: ${bounds.x + bounds.width / 2}px;
transform: translateX(-50%);
padding: 6px;
background: #808080;
border-radius: 10px;
font-size: 14px;
color: white;">Check it out, it is right above this text
</div>
`, { duration: 2000 });
await page.screencast.stop();
}
```
Embrace creativity, overlays are powerful.
### Overlay API Summary
| Method | Use Case |
|--------|----------|
| `page.screencast.showChapter(title, { description?, duration?, styleSheet? })` | Full-screen chapter card with blurred backdrop — ideal for section transitions |
| `page.screencast.showOverlay(html, { duration? })` | Custom HTML overlay — use for callouts, labels, highlights |
| `disposable.dispose()` | Remove a sticky overlay added without duration |
| `page.screencast.hideOverlays()` / `page.screencast.showOverlays()` | Temporarily hide/show all overlays |
## Tracing vs Video
| Feature | Video | Tracing |
|---------|-------|---------|
| Output | WebM file | Trace file (viewable in Trace Viewer) |
| Shows | Visual recording | DOM snapshots, network, console, actions |
| Use case | Demos, documentation | Debugging, analysis |
| Size | Larger | Smaller |
## Limitations
- Recording adds slight overhead to automation
- Large recordings can consume significant disk space
-22
View File
@@ -1,22 +0,0 @@
[mcp_servers.playwright]
command = "mise"
args = [
"exec",
"--",
"node",
"node_modules/@playwright/mcp/cli.js",
"--headless",
"--browser",
"chromium",
"--sandbox",
"--isolated",
"--output-dir",
"../.artifacts/playwright-mcp",
"--output-max-size",
"104857600",
]
cwd = "web"
enabled = true
required = false
startup_timeout_sec = 30
tool_timeout_sec = 120
+13
View File
@@ -0,0 +1,13 @@
{
"browser": {
"browserName": "chromium",
"isolated": true,
"launchOptions": {
"channel": "chromium",
"headless": true,
"chromiumSandbox": true
}
},
"outputDir": ".artifacts/playwright-cli",
"outputMaxSize": 104857600
}
+3 -1
View File
@@ -42,7 +42,9 @@ are available while their APIs and large-file behavior continue to evolve.
The frontend uses React + TypeScript and communicates with the backend via HTTP The frontend uses React + TypeScript and communicates with the backend via HTTP
API. Its first milestone supports login, a root file list, single-file upload, API. Its first milestone supports login, a root file list, single-file upload,
and authenticated download. Frontend source is maintained in `web/`. and authenticated download. Frontend source is maintained in `web/`; see
[`web/README.md`](web/README.md) for development, testing, and browser-debugging
instructions.
--- ---
+31
View File
@@ -46,6 +46,10 @@ account, admin, or large-transfer designs.
## 2026-07-14: Project-local Headless Browser Tooling ## 2026-07-14: Project-local Headless Browser Tooling
**Status**: Agent-debugging and browser-version decisions were superseded by
the 2026-07-15 Playwright CLI decision below. The Playwright Test and
project-local resource decisions remain active.
**Context**: Browser behavior needs deterministic E2E coverage and interactive **Context**: Browser behavior needs deterministic E2E coverage and interactive
agent debugging inside a long-lived headless Debian Incus container. Tooling agent debugging inside a long-lived headless Debian Incus container. Tooling
should remain reproducible without placing browser binaries or generated should remain reproducible without placing browser binaries or generated
@@ -69,3 +73,30 @@ artifacts in a developer's home directory.
browser install scripts before browser tests or MCP debugging. browser install scripts before browser tests or MCP debugging.
- Browser failures can retain traces, screenshots, and video under - Browser failures can retain traces, screenshots, and video under
`.artifacts/playwright/` without adding generated files to source control. `.artifacts/playwright/` without adding generated files to source control.
## 2026-07-15: Stable Playwright CLI Agent Debugging
**Context**: The official Playwright MCP package pulled an alpha Playwright
core alongside the stable test runner. That required a second Chromium
revision and expanded the agent tool surface. Playwright 1.61.1 already
provides the same browser-debugging command set through its embedded CLI.
**Decisions**:
| Area | Choice | Guidance |
|------|--------|----------|
| Agent entry point | Stable `playwright cli` behind the `playwright:cli` npm script | Run it from `web/` with `mise exec -- npm run playwright:cli --`; do not use a global install, standalone `@playwright/cli`, `playwright-cli`, or floating `npx`. |
| Version ownership | `@playwright/test` is the only Playwright dependency | E2E tests and interactive debugging use Playwright 1.61.1 and Chromium revision 1228. |
| Agent workflow | Repository Skill in `.agents/skills/playwright-cli` | Start from Playwright's generated Skill, then preserve the MyGO npm wrapper, headless workflow, artifact policy, and debugging principles when updating it. |
| Browser policy | Bundled Chromium, headless, isolated, and sandboxed | `.playwright/cli.config.json` is the shared CLI configuration; do not silently disable the Chromium sandbox. |
| Local resources | Repository `.cache/` and `.artifacts/` directories | `mise.toml` redirects XDG, npm, and browser caches; CLI output is limited to 100 MB under `.artifacts/playwright-cli/`. |
**Consequences**:
- A fresh environment needs one locked npm install, the Debian browser
libraries, and the stable Chromium install before E2E or CLI use.
- Changing Playwright's CLI entry point only requires changing the npm script;
the repository Skill and operator commands remain stable.
- The official generated references remain available through progressive
disclosure, while the main Skill carries only the MyGO-specific workflow.
- The MCP server configuration, alpha Playwright core, and Chromium revision
1232 are no longer required.
+1 -1
View File
@@ -37,7 +37,7 @@ The project foundation contains:
- Oxlint from the Vite template - Oxlint from the Vite template
- Vitest for framework-independent client and session tests - Vitest for framework-independent client and session tests
- Playwright Test with a headless Chromium smoke test - Playwright Test with a headless Chromium smoke test
- Project-scoped Playwright MCP for interactive browser debugging - Project-scoped Playwright CLI and repository Skill for interactive browser debugging
Ant Design owns reusable UI components and theme tokens. Tailwind CSS is initially limited to application layout, spacing, and responsive utilities. Ant Design owns reusable UI components and theme tokens. Tailwind CSS is initially limited to application layout, spacing, and responsive utilities.
+1
View File
@@ -3,5 +3,6 @@ go = "1.26.2"
node = "24" node = "24"
[env] [env]
XDG_CACHE_HOME = "{{config_root}}/.cache"
NPM_CONFIG_CACHE = "{{config_root}}/.cache/npm" NPM_CONFIG_CACHE = "{{config_root}}/.cache/npm"
PLAYWRIGHT_BROWSERS_PATH = "{{config_root}}/.cache/ms-playwright" PLAYWRIGHT_BROWSERS_PATH = "{{config_root}}/.cache/ms-playwright"
+24 -22
View File
@@ -4,8 +4,7 @@ MyGO's browser client is a pure client-side rendered application built with Reac
## Development ## Development
Install the repository-pinned Go and Node.js versions from the repository Install the repository-pinned Go and Node.js versions from the repository root:
root:
```bash ```bash
mise install mise install
@@ -32,16 +31,11 @@ mise exec -- npm ci
mise exec -- npm run dev mise exec -- npm run dev
``` ```
Vite proxies `/api` to `http://127.0.0.1:10086`. Production deployments must Vite proxies `/api` to `http://127.0.0.1:10086`. Production deployments must serve the static build and `/api/v1` from the same origin, normally through a reverse proxy.
serve the static build and `/api/v1` from the same origin, normally through a
reverse proxy.
## Browser Testing and Debugging ## Browser Testing and Debugging
The repository uses Playwright Test for repeatable browser tests and the The Web project uses the Playwright `@playwright/test` for both repeatable E2E tests and interactive browser debugging. The npm cache, browser binaries, CLI daemon state, screenshots, traces, and videos stay under the repository-level ignored `.cache/` and `.artifacts/` directories.
official Playwright MCP server for interactive browser debugging. Mise keeps
the npm cache and Playwright browser binaries under the repository-level
`.cache/` directory.
Install the Debian browser dependencies once per development container: Install the Debian browser dependencies once per development container:
@@ -49,16 +43,15 @@ Install the Debian browser dependencies once per development container:
mise exec -- npm run playwright:install:deps mise exec -- npm run playwright:install:deps
``` ```
Install the Chromium revisions used by Playwright Test and Playwright MCP: Install the matching bundled Chromium revision:
```bash ```bash
mise exec -- npm run playwright:install mise exec -- npm run playwright:install
mise exec -- npm run playwright:install:mcp
``` ```
The two commands are intentionally separate because the locked stable test E2E tests and interactive debugging intentionally share the same stable Playwright core and browser revision.
runner and MCP package currently depend on different Playwright revisions.
Both revisions are stored in `.cache/ms-playwright/` and are ignored by Git. Currently Playwright's version is locked by the only `@playwright/test` dependency. Standalone `@playwright/cli` or `@playwright/mcp` won't be added unless additional requirement.
Run the headless browser tests with Chromium sandboxing enabled: Run the headless browser tests with Chromium sandboxing enabled:
@@ -66,10 +59,22 @@ Run the headless browser tests with Chromium sandboxing enabled:
mise exec -- npm run test:e2e mise exec -- npm run test:e2e
``` ```
Codex loads the project-scoped Playwright MCP server from Run interactive commands through the repository wrapper:
`.codex/config.toml` after the repository is trusted and Codex is restarted.
The MCP browser uses bundled Chromium in headless, isolated, sandboxed mode; ```bash
its generated files are kept under `.artifacts/playwright-mcp/`. mise exec -- npm run playwright:cli -- open http://127.0.0.1:5173/login
mise exec -- npm run playwright:cli -- snapshot
mise exec -- npm run playwright:cli -- console
mise exec -- npm run playwright:cli -- requests
mise exec -- npm run playwright:cli -- screenshot --filename=.artifacts/playwright-cli/login.png
mise exec -- npm run playwright:cli -- close
```
The wrapper changes to the repository root before invoking the stable `playwright cli` bundled with the locked test runner. This loads `.playwright/cli.config.json`, which selects bundled Chromium in headless, isolated, sandboxed mode and limits generated output under `.artifacts/playwright-cli/` to 100 MB.
Do not invoke `playwright-cli`, a global Playwright installation, or a floating `npx` package.
If the upstream CLI entry point changes, update only the `playwright:cli` npm script. Agents should follow the repository-scoped `$playwright-cli` Skill in `.agents/skills/playwright-cli/` for snapshots, sessions, network diagnosis, test debugging, and artifact handling.
## Checks ## Checks
@@ -77,7 +82,4 @@ its generated files are kept under `.artifacts/playwright-mcp/`.
mise exec -- npm run check mise exec -- npm run check
``` ```
The production build is emitted to `dist/` as static assets. Browser E2E tests The production build is emitted to `dist/` as static assets. Browser E2E tests are run separately because they require the browser setup above. See `docs/web/roadmap.md` at the repository root for architecture boundaries and planned dependencies.
are run separately because they require the browser setup above. See
`docs/web/roadmap.md` at the repository root for architecture boundaries and
planned dependencies.
-65
View File
@@ -16,7 +16,6 @@
"react-router": "^8.2.0" "react-router": "^8.2.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/mcp": "0.0.78",
"@playwright/test": "1.61.1", "@playwright/test": "1.61.1",
"@tailwindcss/vite": "^4.3.2", "@tailwindcss/vite": "^4.3.2",
"@types/node": "^24.13.2", "@types/node": "^24.13.2",
@@ -583,23 +582,6 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
} }
}, },
"node_modules/@playwright/mcp": {
"version": "0.0.78",
"resolved": "https://registry.npmmirror.com/@playwright/mcp/-/mcp-0.0.78.tgz",
"integrity": "sha512-XLTUeA6mEN9sQ+hJ4dfG8EIkDbxS0K3Trc2RBkUJuf02TgE2FQRNTMtq/aJfhyRMINsRl/Ybc4sxcWLtFn4/TQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.62.0-alpha-1783623505000",
"playwright-core": "1.62.0-alpha-1783623505000"
},
"bin": {
"playwright-mcp": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.61.1", "version": "1.61.1",
"resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.61.1.tgz", "resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.61.1.tgz",
@@ -2759,53 +2741,6 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/playwright": {
"version": "1.62.0-alpha-1783623505000",
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.62.0-alpha-1783623505000.tgz",
"integrity": "sha512-6KV9h4PP3hqu4NaGdxxcijWfYh9LJcFI/R2sP4TTC4I5cFo3oRawN0ETlW5MkE3cQEgKhhoj0KUNz4sfpCT0Tg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.62.0-alpha-1783623505000"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.62.0-alpha-1783623505000",
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.62.0-alpha-1783623505000.tgz",
"integrity": "sha512-CPJZdsA/KGT2QQlekiV6Wt+QlQrZHVSZ6oiNtOI/bYYOIVLM8jfKGWTM4zQiyd4UN+40Cq4cA6lxmZHZbtPvJQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.19", "version": "8.5.19",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.19.tgz", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.19.tgz",
+1 -2
View File
@@ -13,7 +13,7 @@
"lint": "oxlint", "lint": "oxlint",
"playwright:install": "playwright install chromium", "playwright:install": "playwright install chromium",
"playwright:install:deps": "playwright install-deps chromium", "playwright:install:deps": "playwright install-deps chromium",
"playwright:install:mcp": "node node_modules/playwright/cli.js install chromium", "playwright:cli": "cd .. && playwright cli",
"test": "vitest run", "test": "vitest run",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"typecheck": "tsc -b", "typecheck": "tsc -b",
@@ -29,7 +29,6 @@
"react-router": "^8.2.0" "react-router": "^8.2.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/mcp": "0.0.78",
"@playwright/test": "1.61.1", "@playwright/test": "1.61.1",
"@tailwindcss/vite": "^4.3.2", "@tailwindcss/vite": "^4.3.2",
"@types/node": "^24.13.2", "@types/node": "^24.13.2",