Skip to content

Commit 3662ef8

Browse files
committed
docs(claude-code): document executablePath wiring for every Playwright consumer (#91)
- README Playwright Strategy: replace the single playwright.config.ts snippet with a per-consumer wiring matrix (table) covering @playwright/ test, @vitest/browser-playwright (incl. @storybook/addon-vitest), direct playwright-core / playwright use, and agent-browser. Each gets its own pasteable snippet. Includes the misleading "npx playwright install" symptom + correct fix for the vitest case. - Dockerfile (default target): add AGENT_BROWSER_EXECUTABLE_PATH=/usr/ bin/chromium so agent-browser shares the system chromium. agent-browser is a Rust CLI with its own env-var family; PLAYWRIGHT_* vars are ignored. Eliminates auto-detection ambiguity and prevents a Chrome-for- Testing download on first run (which would fail in firewalled setups and waste bandwidth elsewhere). - README skills section: rename "Optional: Claude Code skill for Playwright" to "Recommended:" — the wiring matrix is now documented, but the skill encodes how Claude Code drives those entry points. - sandbox-playwright SKILL.md: add @vitest/browser-playwright and direct playwright-core to the path-picker table; expand discovery step #1 to find vitest.config.* alongside playwright.config.* (RTK-safe single- predicate finds); add the "don't run npx playwright install" red flag + fix instruction; broaden the maintainer footer to enumerate the full env-var family (including AGENT_BROWSER_EXECUTABLE_PATH). - CI verify: assert AGENT_BROWSER_EXECUTABLE_PATH is set to /usr/bin/ chromium in the published default image (not added to sandbox — agent- browser isn't installed there).
1 parent 73765a7 commit 3662ef8

4 files changed

Lines changed: 112 additions & 20 deletions

File tree

‎.github/workflows/build-claude-code.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ jobs:
7676
matrix:
7777
include:
7878
- image-suffix: claude-code
79-
verify-command: "bun --version || true && claude --version && mise --version && fish --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp"
79+
verify-command: "bun --version || true && claude --version && mise --version && fish --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test \"$AGENT_BROWSER_EXECUTABLE_PATH\" = /usr/bin/chromium"
8080
runner: ubuntu-24.04
8181
arch: amd64
8282
- image-suffix: claude-code
83-
verify-command: "bun --version || true && claude --version && mise --version && fish --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp"
83+
verify-command: "bun --version || true && claude --version && mise --version && fish --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test \"$AGENT_BROWSER_EXECUTABLE_PATH\" = /usr/bin/chromium"
8484
runner: ubuntu-24.04-arm
8585
arch: arm64
8686
- image-suffix: claude-code-sandbox

‎claude-code/.claude/skills/sandbox-playwright/SKILL.md‎

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,39 @@ metadata:
99

1010
# Playwright in This Sandbox
1111

12-
Playwright is reachable through **two independent paths**:
12+
Playwright is reachable through **multiple independent paths**, any combination of which a project may use:
1313

1414
- **MCP plugin** (`playwright@claude-plugins-official`) — interactive, ad-hoc browser work.
1515
- **`@playwright/test` CLI** — the project's scripted E2E suite.
16+
- **`@vitest/browser-playwright`** — vitest browser mode (used by `@storybook/addon-vitest` for storybook interaction tests, by `@vitest/browser` for browser-mode unit tests).
17+
- **Direct `playwright-core` / `playwright`** — custom scripts that import `chromium` and call `chromium.launch(...)` themselves (CI scrapers, codegen).
1618

1719
Don't install anything. Don't spawn a dev server — the user keeps one running on a port they chose.
1820

19-
## Two paths — pick the right one
21+
## Pick the right path
2022

2123
| Goal | Use | How |
2224
|---|---|---|
2325
| Ad-hoc: "does this button work?", "take a screenshot" | **MCP plugin** | `ToolSearch` → `browser_navigate` → `browser_snapshot` / `browser_click` |
24-
| Reproducible test suite | **Project CLI** | `bun run test:e2e` (whatever the script in the project's `package.json` is) |
26+
| Reproducible E2E suite | **`@playwright/test` CLI** | `bun run test:e2e` (whatever the script in the project's `package.json` is) |
27+
| Storybook interaction tests / vitest browser tests | **`@vitest/browser-playwright`** | The project's `vitest`/`storybook` test script (e.g. `bun run test`, `bun run test-storybook`). Config lives in `vitest.config.ts`, NOT `playwright.config.ts`. |
28+
| Custom script with `chromium.launch(...)` | **Direct** | Run the script as the project does. Requires `executablePath` wired at the call site (image-set env var). |
2529

2630
## Discovery first — never hardcode
2731

2832
Every concrete fact in this section must come from the project at invocation time, not from this skill's prose. Stale skills are worse than missing ones.
2933

30-
1. **Find the Playwright config.** RTK rewrites `find` and rejects compound predicates (`-not`, `! -path`), so use plain `find` and filter with `grep` — works whether RTK is in the loop or not.
34+
1. **Find the relevant config.** RTK rewrites `find` and rejects compound predicates (`-not`, `! -path`, also `\( … -o … \)` grouping), so use plain single-predicate `find` and filter with `grep` — works whether RTK is in the loop or not.
3135
```sh
36+
# For E2E ("run the e2e suite"):
3237
find . -maxdepth 6 -name 'playwright.config.*' | grep -v node_modules
38+
# For storybook / vitest browser tests:
39+
find . -maxdepth 6 -name 'vitest.config.*' | grep -v node_modules
3340
```
34-
- 0 matches → tell the user there's no e2e suite. Stop.
35-
- 1 match → use it.
36-
- 2+ matches → ask which one (a monorepo can ship one config per service, e.g. one for the React app, one for the marketing site).
41+
- 0 `playwright.config.*` matches AND the user asked for E2E → tell them there's no Playwright E2E suite. Stop.
42+
- 0 `vitest.config.*` matches with `test.browser.enabled = true` AND the user asked for storybook/vitest browser tests → tell them browser-mode isn't configured. Stop.
43+
- 2+ matches of the same kind → ask which one (a monorepo can ship one config per service, e.g. one for the React app, one for the marketing site).
44+
- Note: a plain unit-test `vitest.config.*` (no `test.browser.enabled`) doesn't drive Playwright — read the file to confirm browser mode is on before assuming.
3745

3846
2. **Read the config.** Note `testDir`, the entries in `projects[]` (with their `testMatch` / `testIgnore` patterns — they are usually regexes, not filename allowlists), and `webServer` (its `url` and `reuseExistingServer`).
3947

@@ -83,6 +91,7 @@ All tools are registered with the prefix `mcp__plugin_playwright_playwright__`.
8391
| `bun add @playwright/test` or any "reinstall Playwright" attempt | Already installed. Reinstalling risks an unintended version bump and violates the "no unauthorized installs" rule in the project's CLAUDE.md. |
8492
| Spawn your own `bun run dev` | The user keeps one running; Playwright's `webServer` reuses it (`reuseExistingServer: true`). A duplicate port-collides. |
8593
| Hardcode a port (`5173`, `5179`, etc.) in URLs, examples, or docs | Discover it per Step 3. Different projects use different env-var conventions (`APP_PORT`, `PORT`, `VITE_PORT`, …) and the user may override the default. |
94+
| Run `npx playwright install` when a Vitest/Storybook test fails with `Executable doesn't exist at /home/node/.cache/ms-playwright/...` | The image deliberately ships system Chromium at `/usr/bin/chromium` and sets `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1`. The fix is to wire `launchOptions.executablePath` in `vitest.config.ts` (`provider: playwright({ launchOptions: { executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH } })`), not to download a Playwright-managed binary. |
8695
| Call `mcp__plugin_playwright_playwright__browser_*` without `ToolSearch` first | Deferred tools — raw invocation returns `InputValidationError` because the parameter schema hasn't been loaded. |
8796
| Navigate to non-localhost, non-GitHub, non-npm URLs and expect them to work | The devcontainer firewall (see `.claude/rules/devcontainer.md`) blocks arbitrary external domains. Localhost is fine. |
8897
| Chain actions without re-observing | Page state is implicit. Re-`browser_snapshot` or `browser_wait_for` between interactions. |
@@ -92,14 +101,22 @@ All tools are registered with the prefix `mcp__plugin_playwright_playwright__`.
92101

93102
- "Playwright isn't available in this sandbox / devcontainer"
94103
- "I need to install Playwright first"
104+
- "Let me run `npx playwright install`" (the bundled binary is deliberately not used; wire `launchOptions.executablePath` instead)
95105
- "Let me start a dev server on :<some-port>" — you don't know the user's port
96106
- "I'll use `WebFetch` to check the rendered page instead"
97107

98108
All mean: do the discovery in §"Discovery first", then follow the interactive or scripted workflow above. If the dev server isn't running, ask the user to start it — don't start one yourself.
99109

100-
## How the MCP plugin is wired here
110+
## How Playwright is wired here
101111

102-
For maintainers reading this: the upstream devcontainer image (`gatezh/devcontainers` claude-code, both `default` and `sandbox` targets) ships system chromium at `/usr/bin/chromium` and sets `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` + `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium`. Playwright doesn't read that env var on its own — it's a project convention — so:
112+
For maintainers reading this: the upstream devcontainer image (`gatezh/devcontainers` claude-code, both `default` and `sandbox` targets) ships system chromium at `/usr/bin/chromium` and sets:
103113

104-
- The project's `playwright.config.ts` files wire it via `launchOptions.executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` so `playwright test` (or whatever the project's test script wraps) finds the binary.
105-
- `@playwright/mcp` defaults to the `chrome` channel (Google Chrome stable at `/opt/google/chrome/chrome`), which isn't installed. The bundled `.mcp.json` is rewritten by `/usr/local/bin/patch-playwright-mcp` (baked into the image) to launch with `--browser chromium --executable-path /usr/bin/chromium --no-sandbox --headless`. It runs from `init-plugins.sh` (postCreate) AND from `postStartCommand` so plugin auto-updates between sessions don't leave fresh cache dirs unpatched. See gatezh/devcontainers#85, #87.
114+
- `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` — keeps `npm install` from downloading bundled browsers.
115+
- `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium` — project convention; Playwright doesn't read it automatically, so each consumer of `@playwright/test`, `@vitest/browser-playwright`, or direct `playwright-core` use must pass it explicitly through `launchOptions.executablePath`.
116+
- `AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium` (default target only) — `agent-browser` is a Rust CLI with its own env-var family; without this it would auto-detect or attempt a Chrome-for-Testing download on first use.
117+
118+
Beyond those env vars:
119+
120+
- The project's `playwright.config.ts` files wire `launchOptions.executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` so `playwright test` finds the binary.
121+
- The project's `vitest.config.ts` (if it uses `@vitest/browser-playwright`, which `@storybook/addon-vitest` does) wires `provider: playwright({ launchOptions: { executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH } })`.
122+
- `@playwright/mcp` defaults to the `chrome` channel (Google Chrome stable at `/opt/google/chrome/chrome`), which isn't installed. The bundled `.mcp.json` is rewritten by `/usr/local/bin/patch-playwright-mcp` (baked into the image) to launch with `--browser chromium --executable-path /usr/bin/chromium --no-sandbox --headless`. It runs from `init-plugins.sh` (postCreate) AND from `postStartCommand` so plugin auto-updates between sessions don't leave fresh cache dirs unpatched. See gatezh/devcontainers#85, #87, #91.

‎claude-code/.devcontainer/Dockerfile‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,16 @@ USER node
187187
# Avoids version coupling between @playwright/mcp (alpha playwright-core builds)
188188
# and cached browser binaries. Projects using @playwright/test must point their
189189
# playwright.config.ts at process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH.
190+
# AGENT_BROWSER_EXECUTABLE_PATH points agent-browser at the same binary —
191+
# agent-browser is a Rust CLI with its own env-var convention, NOT Playwright,
192+
# so PLAYWRIGHT_* vars are silently ignored. Without this, agent-browser would
193+
# auto-detect or attempt a Chrome-for-Testing download on first use.
190194
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \
191-
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
195+
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium \
196+
AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium
192197

193198
# agent-browser: headless browser automation for AI agents.
194-
# Uses the apt-installed chromium above.
199+
# Uses the apt-installed chromium above (via AGENT_BROWSER_EXECUTABLE_PATH).
195200
# Installed as the node user (see claude-code install above for rationale).
196201
ARG AGENT_BROWSER_VERSION=latest
197202
RUN npm install -g agent-browser@${AGENT_BROWSER_VERSION}

‎claude-code/README.md‎

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ The sandbox firewall blocks vendor doc sites, so Claude Code can't `WebFetch` or
120120
121121
Copy `.claude/skills/sandbox-fetch-docs/` into your project's `.claude/skills/` directory so Claude Code picks it up automatically.
122122
123-
### Optional: Claude Code skill for Playwright
123+
### Recommended: Claude Code skill for Playwright
124124
125-
Both image variants ship system chromium and the `/usr/local/bin/patch-playwright-mcp` binary, so Playwright MCP and `@playwright/test` work out of the box once a project's `playwright.config.ts` wires `launchOptions.executablePath` (see [Playwright Strategy](#playwright-strategy)). The image includes a [sandbox-playwright](.claude/skills/sandbox-playwright/SKILL.md) skill that teaches Claude Code how to drive both paths — discovery-driven, with no hardcoded project ports, service names, or test layouts.
125+
Both image variants ship system chromium and the `/usr/local/bin/patch-playwright-mcp` binary, so Playwright MCP and `@playwright/test` (and `@vitest/browser-playwright`, and direct `playwright-core` calls) work out of the box once each project entry point wires `launchOptions.executablePath` — see [Playwright Strategy](#playwright-strategy) for the full per-consumer wiring matrix. The image includes a [sandbox-playwright](.claude/skills/sandbox-playwright/SKILL.md) skill that teaches Claude Code how to drive these paths — discovery-driven, with no hardcoded project ports, service names, or test layouts.
126126
127127
Copy `.claude/skills/sandbox-playwright/` into your project's `.claude/skills/` directory so Claude Code picks it up automatically.
128128
@@ -240,14 +240,27 @@ The Dockerfile sets:
240240
```bash
241241
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
242242
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
243+
AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium # default target only
243244
```
244245
245246
`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` is a project convention — Playwright
246-
does **not** read it automatically. Each project's `playwright.config.ts`
247-
must honor it:
247+
does **not** read it automatically. Every Playwright entry point in your
248+
project must honor it. The image's job ends at "the env var points at the
249+
binary"; consumer projects do the wiring.
250+
251+
| Consumer | Where to wire | What to add |
252+
|---|---|---|
253+
| `@playwright/mcp` | nothing (image-baked) | `/usr/local/bin/patch-playwright-mcp` rewrites every cached `.mcp.json` to use the system chromium. See [Playwright MCP plugin](#playwright-mcp-plugin) below. |
254+
| `@playwright/test` | `playwright.config.ts` | `use.launchOptions.executablePath` |
255+
| `@vitest/browser-playwright` (incl. `@storybook/addon-vitest`, `@vitest/browser`) | `vitest.config.ts` | `playwright({ launchOptions: { executablePath } })` |
256+
| Direct `playwright-core` / `playwright` use in scripts | every `chromium.launch(...)` site | `chromium.launch({ executablePath })` |
257+
| `agent-browser` (default target only) | nothing | Image sets `AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium`. Independent of the `PLAYWRIGHT_*` convention — agent-browser is a Rust CLI with its own env-var family. |
258+
259+
### `@playwright/test` — `playwright.config.ts`
248260
249261
```ts
250-
// playwright.config.ts
262+
import { defineConfig } from "@playwright/test";
263+
251264
export default defineConfig({
252265
use: {
253266
launchOptions: {
@@ -257,6 +270,63 @@ export default defineConfig({
257270
});
258271
```
259272
273+
In a monorepo, repeat this in every `playwright.config.ts` (one per service
274+
that has a Playwright suite). The same env-var value works for all.
275+
276+
### `@vitest/browser-playwright` — `vitest.config.ts`
277+
278+
The provider's options match the Playwright `LaunchOptions` interface
279+
verbatim under the `launchOptions` key (see the `PlaywrightProviderOptions`
280+
interface in `@vitest/browser-playwright`):
281+
282+
```ts
283+
import { defineConfig, mergeConfig } from "vitest/config";
284+
import { playwright } from "@vitest/browser-playwright";
285+
import viteConfig from "./vite.config";
286+
287+
export default mergeConfig(
288+
viteConfig,
289+
defineConfig({
290+
test: {
291+
browser: {
292+
enabled: true,
293+
headless: true,
294+
provider: playwright({
295+
launchOptions: {
296+
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
297+
},
298+
}),
299+
instances: [{ browser: "chromium" }],
300+
},
301+
},
302+
}),
303+
);
304+
```
305+
306+
For non-Vite projects, drop the `mergeConfig(viteConfig, …)` wrapper —
307+
`defineConfig({ test: { browser: { … } } })` works directly.
308+
309+
**Symptom when this is missing:** `Executable doesn't exist at
310+
/home/node/.cache/ms-playwright/chromium_headless_shell-<rev>/chrome-linux/headless_shell`
311+
followed by a "Please run `npx playwright install`" message. **Don't follow
312+
that hint** — wire `launchOptions.executablePath` instead. Running `npx
313+
playwright install` would re-download the bundled binary the image
314+
deliberately avoids.
315+
316+
### Direct `playwright-core` / `playwright` use
317+
318+
For CI scripts, custom E2E that doesn't use `@playwright/test`, or codegen
319+
sites that import `chromium` directly:
320+
321+
```ts
322+
import { chromium } from "playwright-core"; // or "playwright"
323+
324+
const browser = await chromium.launch({
325+
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
326+
headless: true,
327+
});
328+
```
329+
260330
### Playwright MCP plugin
261331
262332
The `playwright@claude-plugins-official` plugin's `@playwright/mcp` defaults

0 commit comments

Comments
 (0)