diff --git a/.claude/skills/playwright-api/SKILL.md b/.claude/skills/playwright-api/SKILL.md deleted file mode 100644 index b88f86e7c0401..0000000000000 --- a/.claude/skills/playwright-api/SKILL.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: playwright-api -description: Explains how to add playwright API methods. ---- - -# API - -## Adding and modifying APIs -- Before performing the implementation, go over the steps to understand and plan the work ahead. It is important to follow the steps in order, as some of them are prerequisites for others. -- Define (or update) API in `docs/api/class-xxx.md`. For the new methods, params and options use the version from package.json (without -next). -- Watch will kick in and re-generate types for the API -- Implement the new API in `packages/playwright/src/client/xxx.ts` -- Define (or update) channel for the API in `packages/protocol/src/protocol.yml` as needed -- Watch will kick in and re-generate types for protocol channels -- Implement dispatcher handler in `packages/playwright/src/server/dispatchers/xxxDispatcher.ts` as needed -- Handler should just route the call into the corresponding method in `packages/playwright-core/src/server/xxx.ts` -- Place new tests in `tests/page/xxx.spec.ts` or create new test file if needed - -# Build -- Assume watch is running and everything is up to date. - -# Test -- If your tests are only using page, prefer to place them in `tests/page/xxx.spec.ts` and use page fixture. If you need to use browser context, place them in `tests/library/xxx.spec.ts`. -- Run npm test as `npm run ctest ` - -# Lint -- In the end lint via `npm run flint`. diff --git a/.claude/skills/playwright-dev/SKILL.md b/.claude/skills/playwright-dev/SKILL.md new file mode 100644 index 0000000000000..cd21e9dcf4519 --- /dev/null +++ b/.claude/skills/playwright-dev/SKILL.md @@ -0,0 +1,19 @@ +--- +name: playwright-dev +description: Explains how to develop Playwright - add APIs, MCP tools, CLI commands, and vendor dependencies. +--- + +# Playwright Development Guide + +## Table of Contents + +- [Adding and Modifying APIs](api.md) — define API docs, implement client/server, add tests +- [MCP Tools and CLI Commands](mcp-dev.md) — add MCP tools, CLI commands, config options +- [Vendoring Dependencies](vendor.md) — bundle third-party npm packages into playwright-core or playwright + +## Build +- Assume watch is running and everything is up to date. +- If not, run `npm run build`. + +## Lint +- Run `npm run flint` to lint everything before commit. diff --git a/.claude/skills/playwright-dev/api.md b/.claude/skills/playwright-dev/api.md new file mode 100644 index 0000000000000..06431bbe61c62 --- /dev/null +++ b/.claude/skills/playwright-dev/api.md @@ -0,0 +1,293 @@ +# Adding and Modifying APIs + +- Before performing the implementation, go over the steps to understand and plan the work ahead. It is important to follow the steps in order, as some of them are prerequisites for others. + +## Step 1: Define API in Documentation + +Define (or update) API in `docs/src/api/class-xxx.md`. For the new methods, params and options use the version from package.json (without `-next`). + +### Documentation Format + +**Method definition:** +```markdown +## async method: Page.methodName +* since: v1.XX +- returns: <[null]|[Response]> + +Description of the method. + +### param: Page.methodName.paramName +* since: v1.XX +- `paramName` <[string]> + +Description of the parameter. + +### option: Page.methodName.optionName +* since: v1.XX +- `optionName` <[string]> + +Description of the option. +``` + +**Key syntax rules:** +- `* since: v1.XX` — version from package.json (without -next) +- `* langs: js, python` — language filter (optional) +- `* langs: alias-java: navigate` — language-specific method name +- `* deprecated: v1.XX` — deprecation marker +- `<[TypeName]>` — type annotation: `<[string]>`, `<[int]>`, `<[float]>`, `<[boolean]>` +- `<[null]|[Response]>` — union type +- `<[Array]<[Locator]>>` — array type +- `<[Object]>` with indented `- \`field\` <[type]>` — object type +- `### param:` — required parameter +- `### option:` — optional parameter +- `= %%-placeholder-name-%%` — reuse shared param definition from `docs/src/api/params.md` + +**Property definition:** +```markdown +## property: Page.propName +* since: v1.XX +- type: <[string]> + +Description. +``` + +**Event definition:** +```markdown +## event: Page.eventName +* since: v1.XX +- argument: <[Dialog]> + +Description. +``` + +Watch will kick in and auto-generate: +- `packages/playwright-core/types/types.d.ts` — public API types +- `packages/playwright/types/test.d.ts` — test API types + +## Step 2: Implement Client API + +Implement the new API in `packages/playwright-core/src/client/xxx.ts`. + +### Client Implementation Pattern + +Client classes extend `ChannelOwner` and call through `this._channel`: + +```typescript +// Direct channel call (most common) +async methodName(param: string, options: channels.FrameMethodNameOptions = {}): Promise { + await this._channel.methodName({ param, ...options, timeout: this._timeout(options) }); +} + +// Channel call with response wrapping +async goto(url: string, options: channels.FrameGotoOptions = {}): Promise { + return network.Response.fromNullable( + (await this._channel.goto({ url, ...options, timeout: this._timeout(options) })).response + ); +} +``` + +**Key patterns:** +- Parameters are assembled into a single object for the channel call +- Timeout is processed through `this._timeout(options)` or `this._navigationTimeout(options)` +- Return values from channel are unwrapped/converted: `Response.fromNullable()`, `ElementHandle.from()`, etc. +- Locator methods delegate to Frame: `return await this._frame.click(this._selector, { strict: true, ...options })` +- Page methods often delegate to `this._mainFrame` + +## Step 3: Define Protocol Channel + +Define (or update) channel for the API in `packages/protocol/src/protocol.yml` as needed. + +### Protocol YAML Format + +Methods are defined under `commands:` in the interface section: + +```yaml +Page: + type: interface + extends: EventTarget + + commands: + methodName: + title: Short description for tracing + parameters: + url: string # required string + timeout: float # required float + referer: string? # optional string (? suffix) + waitUntil: LifecycleEvent? # optional reference to another type + button: # optional enum + type: enum? + literals: + - left + - right + - middle + modifiers: # optional array of enums + type: array? + items: + type: enum + literals: + - Alt + - Control + - Meta + - Shift + position: Point? # optional reference type + viewportSize: # required inline object + type: object + properties: + width: int + height: int + returns: + response: Response? # optional return value + flags: + slowMo: true + snapshot: true + pausesBeforeAction: true +``` + +**Type primitives:** `string`, `int`, `float`, `boolean`, `binary`, `json` +**Optional:** append `?` to any type: `string?`, `int?`, `object?` +**Arrays:** `type: array` with `items:` (or `type: array?` for optional) +**Enums:** `type: enum` with `literals:` list +**References:** use type name directly: `Response`, `Frame`, `Point` +**Flags:** `slowMo`, `snapshot`, `pausesBeforeAction`, `pausesBeforeInput` + +Watch will kick in and auto-generate: +- `packages/protocol/src/channels.d.ts` — channel TypeScript interfaces +- `packages/playwright-core/src/protocol/validator.ts` — runtime validators +- `packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts` — method metadata + +## Step 4: Implement Dispatcher + +Implement dispatcher handler in `packages/playwright-core/src/server/dispatchers/xxxDispatcher.ts` as needed. + +### Dispatcher Pattern + +Dispatchers receive validated params and route to server objects: + +```typescript +// Simple pass-through (most common) +async methodName(params: channels.PageMethodNameParams, progress: Progress): Promise { + await this._page.methodName(progress, params.value); +} + +// With response wrapping +async goto(params: channels.FrameGotoParams, progress: Progress): Promise { + return { response: ResponseDispatcher.fromNullable(this._browserContextDispatcher, + await this._frame.goto(progress, params.url, params)) }; +} + +// With dispatcher extraction (when params contain dispatcher references) +async expectScreenshot(params: channels.PageExpectScreenshotParams, progress: Progress): Promise { + const mask = (params.mask || []).map(({ frame, selector }) => ({ + frame: (frame as FrameDispatcher)._object, + selector, + })); + return await this._page.expectScreenshot(progress, { ...params, mask }); +} + +// With array result wrapping +async querySelectorAll(params: channels.FrameQuerySelectorAllParams, progress: Progress): Promise { + const elements = await progress.race(this._frame.querySelectorAll(params.selector)); + return { elements: elements.map(e => ElementHandleDispatcher.from(this, e)) }; +} +``` + +**Key patterns:** +- Method signature: `async method(params: channels.XxxMethodParams, progress: Progress): Promise` +- Extract params: `params.url`, `params.selector`, etc. +- Convert dispatcher refs to server objects: `(params.frame as FrameDispatcher)._object` +- Wrap server objects as dispatchers in results: `ResponseDispatcher.fromNullable()`, `ElementHandleDispatcher.from()` +- All methods receive `Progress` for timeout/cancellation + +## Step 5: Implement Server Logic + +Handler should route the call into the corresponding method in `packages/playwright-core/src/server/xxx.ts`. + +Server methods implement the actual browser interaction: + +```typescript +// In packages/playwright-core/src/server/frames.ts +async goto(progress: Progress, url: string, options: types.GotoOptions = {}): Promise { + // ... validation, URL construction ... + // Delegates to browser-specific implementation: + const result = await this._page.delegate.navigateFrame(this, url, referer); + // ... wait for lifecycle events ... + return response; +} +``` + +Browser-specific implementations live in: +- `packages/playwright-core/src/server/chromium/crPage.ts` — Chromium (uses CDP: `this._client.send('Page.navigate', { ... })`) +- `packages/playwright-core/src/server/firefox/ffPage.ts` — Firefox +- `packages/playwright-core/src/server/webkit/wkPage.ts` — WebKit + +## Step 6: Write Tests + +### Test Location +- Page-only tests: `tests/page/xxx.spec.ts` — use `page` fixture +- Context tests: `tests/library/xxx.spec.ts` — use `context` fixture + +### Test Patterns + +**Page test:** +```typescript +import { test as it, expect } from './pageTest'; + +it('should do something @smoke', async ({ page, server }) => { + await page.goto(server.EMPTY_PAGE); + // ... assertions ... + expect(page.url()).toBe(server.EMPTY_PAGE); +}); + +it('should handle options', async ({ page, server, browserName, isAndroid }) => { + it.skip(isAndroid, 'Not supported on Android'); + it.info().annotations.push({ type: 'issue', description: 'https://github.com/user/repo/issues/123' }); + // ... +}); +``` + +**Library/context test:** +```typescript +import { contextTest as it, expect } from '../config/browserTest'; + +it('should work with context', async ({ context, server }) => { + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + // ... +}); +``` + +### Available Fixtures +- `page` — isolated page instance +- `context` — browser context (library tests) +- `server` — HTTP test server (`server.EMPTY_PAGE`, `server.PREFIX`, `server.CROSS_PROCESS_PREFIX`) +- `httpsServer` — HTTPS test server +- `asset(name)` — path to test asset file +- `browserName` — `'chromium' | 'firefox' | 'webkit'` +- `channel` — browser channel string +- `isAndroid`, `isBidi`, `isElectron` — platform booleans +- `isWindows`, `isMac`, `isLinux` — OS booleans +- `mode` — test mode (`'default'`, `'service'`, etc.) + +### Running Tests +```bash +npm run ctest tests/page/xxx.spec.ts # Chromium only +npm run test tests/page/xxx.spec.ts # All browsers +npm run ctest -- --grep "should do something" # Filter by name +``` + +## Architecture Overview + +``` +docs/src/api/class-xxx.md (API documentation — source of truth for public types) + → auto-generates → types.d.ts, test.d.ts + +packages/protocol/src/protocol.yml (RPC protocol definition) + → auto-generates → channels.d.ts, validator.ts, protocolMetainfo.ts + +Client call chain: + user code → Page.method() → Frame.method() → this._channel.method(params) + → Proxy validates & sends → Connection.sendMessageToServer() + → [wire] → + DispatcherConnection.dispatch() → XxxDispatcher.method(params, progress) + → ServerObject.method(progress, ...) → BrowserDelegate (CDP/Firefox/WebKit) +``` diff --git a/.claude/skills/playwright-dev/mcp-dev.md b/.claude/skills/playwright-dev/mcp-dev.md new file mode 100644 index 0000000000000..e022738a5b376 --- /dev/null +++ b/.claude/skills/playwright-dev/mcp-dev.md @@ -0,0 +1,498 @@ +# MCP Tools and CLI Commands + +## Adding MCP Tools + +### Step 1: Create the Tool File + +Create `packages/playwright/src/mcp/browser/tools/.ts`. + +Import zod from the MCP bundle and use `defineTool` or `defineTabTool`: + +```typescript +import { z } from 'playwright-core/lib/mcpBundle'; +import { defineTool, defineTabTool } from './tool'; +``` + +**Choose `defineTabTool` vs `defineTool`:** +- `defineTabTool` — most tools use this. Receives a `Tab` object, auto-handles modal state (dialogs/file choosers). +- `defineTool` — receives the full `Context`. Use when you need `context.ensureBrowserContext()` without a specific tab, or need custom tab management. + +**Tool definition pattern:** + +```typescript +const myTool = defineTabTool({ + capability: 'core', // ToolCapability — see step 2 + + // Optional: only available in skill mode (not exposed via MCP) + // skillOnly: true, + + // Optional: this tool clears a modal state ('dialog' | 'fileChooser') + // clearsModalState: 'dialog', + + schema: { + name: 'browser_my_tool', // MCP tool name (browser_ prefix) + title: 'My Tool', // Human-readable title + description: 'Does something', // Description shown to LLM + inputSchema: z.object({ + ref: z.string().describe('Element reference from snapshot'), + value: z.string().optional().describe('Optional value'), + }), + type: 'action', // 'input' | 'assertion' | 'action' | 'readOnly' + }, + + handle: async (tab, params, response) => { + // Implementation using tab.page (Playwright Page object) + await tab.page.click(`[ref="${params.ref}"]`); + + // Add generated Playwright code + response.addCode(`await page.click('[ref="${params.ref}"]');`); + + // Include page snapshot in response (for navigation/state changes) + response.setIncludeSnapshot(); + + // Or add text result + response.addTextResult('Done'); + }, +}); + +export default [myTool]; +``` + +**Schema type values:** +- `'action'` — state-changing operations (navigate, click, fill) +- `'input'` — user input (typing, keyboard) +- `'readOnly'` — queries that don't modify state (list cookies, get snapshot) +- `'assertion'` — testing/verification tools + +**Response API:** +- `response.addTextResult(text)` — add text to result section +- `response.addError(error)` — add error message +- `response.addCode(code)` — add generated Playwright code snippet +- `response.setIncludeSnapshot()` — include ARIA snapshot in response +- `response.setIncludeFullSnapshot(filename?)` — force full snapshot +- `response.addResult(title, data, fileTemplate)` — add file result +- `response.registerImageResult(data, 'png'|'jpeg')` — add image + +**Context tool example** (for browser-context-level operations): + +```typescript +const myContextTool = defineTool({ + capability: 'storage', + schema: { /* ... */ type: 'readOnly' }, + + handle: async (context, params, response) => { + const browserContext = await context.ensureBrowserContext(); + const cookies = await browserContext.cookies(); + response.addTextResult(cookies.map(c => `${c.name}=${c.value}`).join('\n')); + }, +}); +``` + +### Step 2: Add ToolCapability (if needed) + +If your tool doesn't fit an existing capability, add a new one to `packages/playwright/src/mcp/config.d.ts`: + +```typescript +export type ToolCapability = + 'config' | + 'core' | // Always enabled + 'core-navigation' | // Always enabled + 'core-tabs' | // Always enabled + 'core-input' | // Always enabled + 'core-install' | // Always enabled + 'network' | + 'pdf' | + 'storage' | + 'testing' | + 'vision' | + 'devtools'; // Add yours here +``` + +**Capability filtering rules:** +- Tools with `core*` capabilities are always enabled +- Other capabilities must be enabled via `--caps` or config `capabilities` array +- `skillOnly: true` tools are only available in skill mode, never via MCP + +### Step 3: Register the Tool + +In `packages/playwright/src/mcp/browser/tools.ts`: + +```typescript +import myTool from './tools/myTool'; + +export const browserTools: Tool[] = [ + // ... existing tools ... + ...myTool, +]; +``` + +### Step 4: Write Tests + +Create `tests/mcp/.spec.ts`. Use the fixtures from `./fixtures`: + +```typescript +import { test, expect } from './fixtures'; + +test('browser_my_tool', async ({ client, server }) => { + // Setup: navigate to a page first + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + // Call your tool + expect(await client.callTool({ + name: 'browser_my_tool', + arguments: { ref: 'e1' }, + })).toHaveResponse({ + code: `await page.click('[ref="e1"]');`, + snapshot: expect.stringContaining('some content'), + }); +}); + +test('browser_my_tool error case', async ({ client }) => { + expect(await client.callTool({ + name: 'browser_my_tool', + arguments: { ref: 'invalid' }, + })).toHaveResponse({ + error: expect.stringContaining('Error:'), + isError: true, + }); +}); +``` + +**Test fixtures:** +- `client` — MCP client, call tools via `client.callTool({ name, arguments })` +- `startClient(options?)` — client factory, for custom config/args/roots +- `server` — HTTP test server (`server.PREFIX`, `server.HELLO_WORLD`, `server.setContent(path, html, contentType)`) +- `httpsServer` — HTTPS test server + +**Custom matchers:** +- `toHaveResponse({ code?, snapshot?, page?, error?, isError?, result?, events?, modalState? })` — matches parsed response sections +- `toHaveTextResponse(text)` — matches raw text with normalization + +**Parsed response sections:** +- `code` — generated Playwright code (without ```js fences) +- `snapshot` — ARIA page snapshot (with ```yaml fences) +- `page` — page info (URL, title) +- `error` — error message +- `result` — text result +- `events` — console messages, downloads +- `modalState` — active dialog/file chooser info +- `tabs` — tab listing +- `isError` — boolean + +### Testing MCP Tools +- Run tests: `npm run ctest-mcp ` +- Do not run `test --debug` + +--- + +## Adding CLI Commands + +CLI commands are thin wrappers over MCP tools. They live in the daemon and map CLI args to MCP tool calls. + +### Step 1: Implement the MCP Tool + +Implement the corresponding MCP tool first (see section above). CLI commands call MCP tools via `toolName`/`toolParams`. + +### Step 2: Add the Command Declaration + +In `packages/playwright/src/cli/daemon/commands.ts`, use `declareCommand()`: + +```typescript +import { z } from 'playwright-core/lib/mcpBundle'; +import { declareCommand } from './command'; + +const myCommand = declareCommand({ + name: 'my-command', // CLI command name (kebab-case) + description: 'Does something', // Shown in help + category: 'core', // Category for help grouping + + // Positional arguments (ordered, parsed from CLI positional args) + args: z.object({ + url: z.string().describe('The URL to navigate to'), + ref: z.string().optional().describe('Optional element reference'), + }), + + // Named options (parsed from --flag or --flag=value) + options: z.object({ + submit: z.boolean().optional().describe('Whether to submit'), + filename: z.string().optional().describe('Output filename'), + }), + + // MCP tool name — string or function for dynamic routing + toolName: 'browser_my_tool', + // OR dynamic: + // toolName: ({ submit }) => submit ? 'browser_submit' : 'browser_type', + + // Map CLI args/options to MCP tool params + toolParams: ({ url, ref, submit, filename }) => ({ + url, + ref, + submit, + filename, + }), +}); +``` + +Then add to the `commandsArray` at the bottom of the file, in the correct category section: + +```typescript +const commandsArray: AnyCommandSchema[] = [ + // core category + open, + close, + // ... existing commands ... + myCommand, // <-- add here in the right category + // ... +]; +``` + +**Categories** (defined in `packages/playwright/src/cli/daemon/command.ts`): + +```typescript +type Category = 'core' | 'navigation' | 'keyboard' | 'mouse' | 'export' | + 'storage' | 'tabs' | 'network' | 'devtools' | 'browsers' | + 'config' | 'install'; +``` + +To add a new category: +1. Add it to `Category` type in `packages/playwright/src/cli/daemon/command.ts` +2. Add it to the `categories` array in `packages/playwright/src/cli/daemon/helpGenerator.ts`: + ```typescript + const categories: { name: Category, title: string }[] = [ + // ... existing ... + { name: 'mycat', title: 'My Category' }, + ]; + ``` + +**Special tool patterns:** +- `toolName: ''` — command handled specially by daemon (e.g., `close`, `list`, `install`) +- Use `numberArg` for numeric CLI args: `x: numberArg.describe('X coordinate')` +- Param renaming: `toolParams: ({ w: width, h: height }) => ({ width, height })` +- Dynamic toolName: `toolName: ({ clear }) => clear ? 'browser_clear' : 'browser_list'` + +### Step 3: Update SKILL File + +Update `packages/playwright/src/skill/SKILL.md` with the new command documentation. +Add reference docs in `packages/playwright/src/skill/references/` if the feature is complex. + +Run `npm run playwright-cli -- --help` to verify the help output includes your new command. + +### Step 4: Write CLI Tests + +Create `tests/mcp/cli-.spec.ts`. Use fixtures from `./cli-fixtures`: + +```typescript +import { test, expect } from './cli-fixtures'; + +test('my-command', async ({ cli, server }) => { + // Open a page first + await cli('open', server.PREFIX); + + // Run your command + const { output, snapshot } = await cli('my-command', 'arg1', '--option=value'); + expect(output).toContain('expected text'); + expect(snapshot).toContain('expected snapshot content'); +}); +``` + +**CLI test fixtures:** +- `cli(...args)` — run CLI command, returns `{ output, error, exitCode, snapshot, attachments }` + - `output` — stdout text + - `snapshot` — extracted ARIA snapshot (if present) + - `attachments` — file attachments `{ name, data }[]` + - `error` — stderr text + - `exitCode` — process exit code + +### Testing CLI Commands +- Run tests: `npm run ctest-mcp cli-` +- Do not run `test --debug` + +--- + +## Adding Config Options + +When you need to add a new config option, update these files in order: + +### 1. Type definition: `packages/playwright/src/mcp/config.d.ts` + +Add the option to the `Config` type with JSDoc: + +```typescript +export type Config = { + // ... existing ... + + /** + * Description of the new option. + */ + myOption?: string; +}; +``` + +### 2. CLI options type: `packages/playwright/src/mcp/browser/config.ts` + +Add to `CLIOptions` type: + +```typescript +export type CLIOptions = { + // ... existing ... + myOption?: string; +}; +``` + +If the option needs to be in `FullConfig` (with required/resolved values), update `FullConfig` and `defaultConfig`: + +```typescript +export type FullConfig = Config & { + // ... existing ... + myOption: string; // required in resolved config +}; + +export const defaultConfig: FullConfig = { + // ... existing ... + myOption: 'default-value', +}; +``` + +### 3. Config from CLI: `configFromCLIOptions()` in `config.ts` + +Map CLI option to config: + +```typescript +const config: Config = { + // ... existing ... + myOption: cliOptions.myOption, +}; +``` + +### 4. Config from env: `configFromEnv()` in `config.ts` + +Add environment variable mapping: + +```typescript +options.myOption = envToString(process.env.PLAYWRIGHT_MCP_MY_OPTION); +// For booleans: envToBoolean(process.env.PLAYWRIGHT_MCP_MY_OPTION) +// For numbers: numberParser(process.env.PLAYWRIGHT_MCP_MY_OPTION) +// For comma lists: commaSeparatedList(process.env.PLAYWRIGHT_MCP_MY_OPTION) +// For semicolon lists: semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_MY_OPTION) +``` + +### 5. MCP server CLI: `packages/playwright/src/mcp/program.ts` + +Add CLI flag: + +```typescript +command + .option('--my-option ', 'description of option') +``` + +### 6. Merge config (if nested) + +If the option is nested, update `mergeConfig()` in `config.ts` to deep-merge it. + +**Config resolution order:** `defaultConfig` → config file → env vars → CLI args (last wins). + +--- + +## SKILL File + +The skill file is located at `packages/playwright/src/skill/SKILL.md`. It contains documentation for all available CLI commands and MCP tools. Update it whenever you add new commands or tools. + +Reference docs live in `packages/playwright/src/skill/references/`: +- `request-mocking.md` — network mocking patterns +- `running-code.md` — code execution +- `session-management.md` — session handling +- `storage-state.md` — state persistence +- `test-generation.md` — test creation +- `tracing.md` — trace recording +- `video-recording.md` — video capture + +Run `npm run playwright-cli -- --help` to see the latest available commands and use them to update the skill file. + +--- + +## Architecture Reference + +### Directory Structure + +``` +packages/playwright/src/ +├── mcp/ +│ ├── browser/ +│ │ ├── tools/ # All MCP tool implementations +│ │ │ ├── tool.ts # Tool/TabTool types, defineTool(), defineTabTool() +│ │ │ ├── common.ts # close, resize +│ │ │ ├── navigate.ts # navigate, goBack, goForward, reload +│ │ │ ├── snapshot.ts # page snapshot +│ │ │ ├── form.ts # click, type, fill, select, check +│ │ │ ├── keyboard.ts # press, keydown, keyup +│ │ │ ├── mouse.ts # mouse move, click, wheel +│ │ │ ├── tabs.ts # tab management +│ │ │ ├── cookies.ts # cookie CRUD +│ │ │ ├── webstorage.ts # localStorage, sessionStorage +│ │ │ ├── storage.ts # storage state save/load +│ │ │ ├── network.ts # network requests listing +│ │ │ ├── route.ts # request mocking/routing +│ │ │ ├── console.ts # console messages +│ │ │ ├── evaluate.ts # JS evaluation +│ │ │ ├── screenshot.ts # screenshots +│ │ │ ├── pdf.ts # PDF generation +│ │ │ ├── files.ts # file upload +│ │ │ ├── dialogs.ts # dialog handling +│ │ │ ├── verify.ts # assertions +│ │ │ ├── wait.ts # wait operations +│ │ │ ├── tracing.ts # trace recording +│ │ │ ├── video.ts # video recording +│ │ │ ├── runCode.ts # run Playwright code +│ │ │ ├── devtools.ts # DevTools integration +│ │ │ ├── config.ts # config tool +│ │ │ ├── install.ts # browser install +│ │ │ └── utils.ts # shared utilities +│ │ ├── tools.ts # Tool registry (browserTools array, filteredTools) +│ │ ├── config.ts # Config resolution, CLIOptions, FullConfig +│ │ ├── context.ts # Browser context management +│ │ ├── response.ts # Response class, parseResponse() +│ │ └── tab.ts # Tab management +│ ├── sdk/ +│ │ ├── server.ts # MCP server +│ │ └── tool.ts # ToolSchema type, toMcpTool() +│ ├── config.d.ts # Config type, ToolCapability type +│ └── program.ts # MCP server CLI setup +├── cli/ +│ ├── client/ +│ │ ├── program.ts # CLI client entry (argument parsing) +│ │ ├── session.ts # Session management +│ │ └── registry.ts # Session registry +│ └── daemon/ +│ ├── command.ts # Category type, CommandSchema, declareCommand(), parseCommand() +│ ├── commands.ts # All CLI command declarations +│ ├── helpGenerator.ts # Help text generation (generateHelp, generateHelpJSON) +│ └── daemon.ts # Daemon server +└── skill/ + ├── SKILL.md # Skill documentation + └── references/ # Reference docs + +tests/mcp/ +├── fixtures.ts # MCP test fixtures (client, startClient, server) +├── cli-fixtures.ts # CLI test fixtures (cli helper) +├── .spec.ts # MCP tool tests +└── cli-.spec.ts # CLI command tests +``` + +### Execution Flow + +``` +MCP Server mode: + LLM → MCP protocol → Server.callTool(name, args) + → zod validates input → Tool.handle(context|tab, params, response) + → response.serialize() → MCP protocol → LLM + +CLI mode: + User → `playwright-cli my-command arg1 --opt=val` + → Client parses with minimist → sends to Daemon via socket + → parseCommand() maps CLI args to MCP tool params via zod + → backend.callTool(toolName, toolParams) + → Response formatted → printed to stdout +``` diff --git a/.claude/skills/playwright-dev/vendor.md b/.claude/skills/playwright-dev/vendor.md new file mode 100644 index 0000000000000..e58a39c945ebf --- /dev/null +++ b/.claude/skills/playwright-dev/vendor.md @@ -0,0 +1,190 @@ +# Vendoring (Bundling) a New Dependency + +Playwright vendors third-party npm packages by bundling them with esbuild into self-contained files. +This isolates dependencies, prevents version conflicts, and keeps the published packages lean. + +## Architecture Overview + +Each bundle lives under `packages//bundles//` and consists of three parts: + +1. **Bundle directory** (`bundles//`) — has its own `package.json` with the dependencies to vendor, plus a `src/BundleImpl.ts` entry point that imports and re-exports them. +2. **Build configuration** in `utils/build/build.js` — an esbuild entry that bundles the impl file into a single minified CJS file. +3. **Wrapper file** (`src/Bundle.ts`) — a thin typed wrapper that `require()`s the built bundle impl and re-exports symbols with TypeScript types. + +Data flow: +``` +bundles//package.json (declares npm deps) + → npm ci → node_modules/ +bundles//src/BundleImpl.ts (imports from node_modules, re-exports) + → esbuild (bundle + minify) → +lib/BundleImpl.js (single self-contained file) + ← +src/Bundle.ts (typed wrapper, require('./...BundleImpl')) + → esbuild (normal compile) → +lib/Bundle.js (used by application code) +``` + +## Step-by-Step: Adding a New Bundle + +### Decide which package it belongs to + +- `packages/playwright-core/bundles/` — for core browser automation deps (networking, compression, protocols, etc.) +- `packages/playwright/bundles/` — for test runner deps (assertion libs, transpilers, file watchers, etc.) + +### 1. Create the bundle directory + +``` +packages//bundles// +├── package.json +└── src/ + └── BundleImpl.ts +``` + +### 2. Create `package.json` + +Minimal private package with only the deps you want to bundle: + +```json +{ + "name": "-bundle", + "version": "0.0.1", + "private": true, + "dependencies": { + "some-lib": "^1.2.3" + }, + "devDependencies": { + "@types/some-lib": "^1.2.0" + } +} +``` + +Then run `npm install` inside the bundle directory to generate `package-lock.json`. + +### 3. Create `src/BundleImpl.ts` + +This is the esbuild entry point. Import from `node_modules` and re-export: + +```typescript +// For default exports: +import someLibrary from 'some-lib'; +export const someLib = someLibrary; + +// For named exports: +export { SomeClass } from 'some-lib'; + +// For namespace imports: +import * as someLibrary from 'some-lib'; +export const someLib = someLibrary; + +// For vendored/third-party code that can't be bundled: +const custom = require('./third_party/custom'); +export const customThing = custom; +``` + +### 4. Register the bundle in `utils/build/build.js` + +Add an entry to the `bundles` array (around line 246): + +```javascript +bundles.push({ + modulePath: 'packages//bundles/', + entryPoints: ['src/BundleImpl.ts'], + // Use outdir for a single .js file alongside other lib files: + outdir: 'packages//lib', + // OR use outfile for output in a subdirectory (needed if bundle has non-JS assets): + // outfile: 'packages//lib/BundleImpl/index.js', + + // Optional: deps that should NOT be bundled (must be installed at runtime): + // external: ['express'], + + // Optional: redirect imports to custom implementations: + // alias: { 'some-module': 'custom-impl.ts' }, +}); +``` + +**`outdir` vs `outfile`:** +- `outdir` — output goes to `lib/BundleImpl.js` (most bundles use this) +- `outfile` — output goes to `lib/BundleImpl/index.js` (use when you need to copy companion files like binaries next to the bundle) + +### 5. Create the typed wrapper `src/Bundle.ts` + +This file lives in the main package source (NOT in the bundle directory). It provides TypeScript types while loading the bundled code at runtime: + +```typescript +// packages//src/Bundle.ts +// (or src/subdir/Bundle.ts if it belongs in a subdirectory) + +export const someLib: typeof import('../bundles//node_modules/some-lib') + = require('./BundleImpl').someLib; + +export const SomeClass: typeof import('../bundles//node_modules/some-lib').SomeClass + = require('./BundleImpl').SomeClass; + +// Re-export types if needed: +export type { SomeType } from '../bundles//node_modules/some-lib'; +``` + +The pattern is: `typeof import('../bundles//node_modules/...')` for the type, `require('./BundleImpl').` for the value. + +If the wrapper lives in a subdirectory (e.g. `src/common/Bundle.ts`), adjust the `outdir` accordingly so the BundleImpl ends up next to the compiled wrapper: +```javascript +// in build.js +outdir: 'packages//lib/common', +``` + +### 6. Build and verify + +```bash +npm run build +``` + +Or if watch is running, it will pick up changes automatically. + +### 7. Use the bundle in application code + +Import from the wrapper file, never from the bundle directory or `node_modules` directly: + +```typescript +import { someLib } from '../Bundle'; +``` + +## Existing Bundles Reference + +### playwright-core bundles + +| Bundle | Deps | Output | +|--------|------|--------| +| `utils` | colors, commander, debug, diff, dotenv, graceful-fs, https-proxy-agent, jpeg-js, mime, minimatch, open, pngjs, progress, proxy-from-env, socks-proxy-agent, ws, yaml | `lib/utilsBundleImpl/index.js` | +| `zip` | yauzl, yazl, get-stream, debug | `lib/zipBundleImpl.js` | +| `mcp` | @modelcontextprotocol/sdk, zod, zod-to-json-schema | `lib/mcpBundleImpl/index.js` | + +### playwright bundles + +| Bundle | Deps | Output | +|--------|------|--------| +| `utils` | chokidar, enquirer, json5, source-map-support, stoppable, unified, remark-parse | `lib/utilsBundleImpl.js` | +| `babel` | ~30 @babel/* packages | `lib/transform/babelBundleImpl.js` | +| `expect` | expect, jest-matcher-utils | `lib/common/expectBundleImpl.js` | + +## Advanced Patterns + +### Adding a dep to an existing bundle + +If the dep logically belongs with an existing bundle (e.g. a new utility lib → `utils` bundle): + +1. Add the dependency to the existing `bundles//package.json` +2. Run `npm install` in that bundle directory +3. Add the import/export to the existing `src/BundleImpl.ts` +4. Add the typed re-export to the existing `src/Bundle.ts` + +### Vendored third-party code + +If a package can't be bundled by esbuild (e.g. it uses dynamic requires or has runtime file dependencies), place a modified copy in `bundles//src/third_party/` and require it from the BundleImpl. See `bundles/zip/src/third_party/extract-zip.js` for an example. + +### External dependencies + +Use `external: ['pkg']` in the build.js config when a dependency should NOT be bundled — e.g. optional peer deps that users install themselves. These must be available at runtime in the consumer's `node_modules`. + +### Module aliases + +Use `alias: { 'module-name': 'local-file.ts' }` to replace a dependency with a custom local implementation. The alias path is relative to the bundle's `modulePath`. See the `mcp` bundle's `raw-body` alias for an example. diff --git a/.claude/skills/playwright-mcp-dev/SKILL.md b/.claude/skills/playwright-mcp-dev/SKILL.md deleted file mode 100644 index 30dd879c34ec4..0000000000000 --- a/.claude/skills/playwright-mcp-dev/SKILL.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -name: playwright-mcp-dev -description: Explains how to add and debug playwright MCP tools and CLI commands. ---- - -# MCP - -## Adding MCP Tools -- Create a new tool in `packages/playwright/src/mcp/browser/tools/your-tool.ts` -- Register the tool in `packages/playwright/src/mcp/browser/tools.ts` -- Add ToolCapability in `packages/playwright/src/mcp/config.d.ts` -- Place new tests in `tests/mcp/mcp-.spec.ts` - -## Building -- Assume watch is running at all times, run lint to see type errors - -## Testing -- Run tests as `npm run ctest-mcp ` -- Do not run test --debug - -# CLI - -## Adding commands -- CLI commands are based on MCP tools. Implement the corresponding MCP tool as per `Adding MCP Tools` section above, if needed. -- Add new CLI category for tool if needed: - - Add Category in `packages/playwright/src/mcp/terminal/command.ts` - - Update doc generator `packages/playwright/src/mcp/terminal/helpGenerator.ts` -- Register command in `packages/playwright/src/mcp/terminal/commands.ts` -- Update skill file at `packages/playwright/src/skill/SKILL.md` and references if necessary - in `packages/playwright/src/skill/references/` -- Place new tests in `tests/mcp/cli-.spec.ts` - -## Adding CLI options or Config options -When you need to add something to config. - -- `packages/playwright/src/mcp/program.ts` - - add CLI option and doc -- `packages/playwright/src/mcp/config.d.ts` - - add and document the option -- `packages/playwright/src/mcp/config.ts` - - modify FullConfig if needed - - and CLIOptions if needed - - add it to configFromEnv - -## Building -- Assume watch is running at all times, run lint to see type errors - -## Testing -- Run tests as `npm run ctest-mcp cli-` -- Do not run test --debug - -# Lint -- run `npm run flint` to lint everything before commit - -# SKILL File - -The skill file is located at `packages/playwright/src/skill/SKILL.md`. It contains documentation for all available CLI commands and MCP tools. Update it whenever you add new commands or tools. -At any point in time you can run "npm run playwright-cli -- --help" to see the latest available commands and use them to update the skill file. diff --git a/packages/devtools/src/devtools.tsx b/packages/devtools/src/devtools.tsx index 5d886e16f3f05..520f7577dba7f 100644 --- a/packages/devtools/src/devtools.tsx +++ b/packages/devtools/src/devtools.tsx @@ -351,7 +351,7 @@ export const DevTools: React.FC<{ wsUrl?: string }> = ({ wsUrl }) => { onKeyDown={onOmniboxKeyDown} onFocus={e => e.target.select()} /> - - {selectedTab?.inspectorUrl && ( + } + {false && selectedTab?.inspectorUrl && (