Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions .claude/skills/playwright-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,11 @@ description: Explains how to develop Playwright - add APIs, MCP tools, CLI comma

# Playwright Development Guide

## Table of Contents
See [CLAUDE.md](../../../CLAUDE.md) for monorepo structure, build/test/lint commands, and coding conventions.

## Detailed Guides

- [Library Architecture](library.md) — client/server/dispatcher structure, protocol layer, DEPS rules
- [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
- [Uploading Fixes to GitHub](github.md) — branch naming, commit format, pushing fixes for issues

## 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.
56 changes: 0 additions & 56 deletions .claude/skills/playwright-dev/github.md

This file was deleted.

2 changes: 2 additions & 0 deletions .claude/skills/playwright-dev/library.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,8 @@ it('should click button', async ({ page, server }) => {
### Running Tests

- `npm run ctest <file>` — runs on Chromium only (fast, use during development)
- `npm run wtest <file>` — runs on WebKit only
- `npm run ftest <file>` — runs on Firefox only
- `npm run test <file>` — runs on all browsers (Chromium, Firefox, WebKit)

Examples:
Expand Down
134 changes: 134 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
### Monorepo Packages

| Package | npm name | Purpose |
|---------|----------|---------|
| `playwright-core` | `playwright-core` | Browser automation engine: client, server, dispatchers, protocol |
| `playwright` | `playwright` | Test runner + browser automation (public package) |
| `playwright-test` | `@playwright/test` | Test runner entry point |
| `playwright-client` | `@playwright/client` | Standalone client package |
| `protocol` | *(internal)* | RPC protocol definitions (`protocol.yml` → generated `channels.d.ts`) |

### Browser Packages

`playwright-chromium`, `playwright-firefox`, `playwright-webkit` — per-browser distributions.
`playwright-browser-chromium`, `playwright-browser-firefox`, `playwright-browser-webkit` — binary packages.

### Tooling Packages

| Package | Purpose |
|---------|---------|
| `html-reporter` | HTML test report viewer |
| `trace-viewer` | Trace viewer UI |
| `recorder` | Test recorder |
| `web` | Shared web UI components |
| `injected` | Scripts injected into browser pages |

### Component Testing

`playwright-ct-core`, `playwright-ct-react`, `playwright-ct-vue`

### Key Directories

| Directory | Purpose |
|-----------|---------|
| `tests/` | All test suites (page, library, playwright-test, mcp, components, etc.) |
| `docs/src/` | API documentation — **source of truth** for public TypeScript types |
| `docs/src/api/` | Per-class API reference (`class-page.md`, `class-locator.md`, etc.) |
| `utils/` | Build scripts, code generation, linting, doc tools |
| `browser_patches/` | Browser engine patches |

## Build

```bash
npm run build # Full build
npm run watch # Watch mode (recommended during development)
```

Assume watch is running and code is up to date. Generated files (types, channels, validators) are produced by watch automatically.

## Lint

```bash
npm run flint
```

Runs all lint checks in parallel: eslint, tsc, doclint, check-deps, generate_channels, generate_types, lint-tests, test-types, lint-packages, code-snippet linting.

**Always run `flint` before committing.** Do not use `tsc --noEmit` or individual lint commands separately.

## Test Commands

| Command | Scope |
|---------|-------|
| `npm run ctest <filter>` | Chromium only library tests — **use during development** |
| `npm run test <filter> -- --project=<chromium,firefix,webkit>` | All library / per project |
| `npm run ttest <filter>` | Test runner (`tests/playwright-test/`) |
| `npm run ctest-mcp <filter>` | Chromium only MCP tools (`tests/mcp/`) |
| `npm run test-mcp <filter> -- --project=<chromium,firefox,webkit>` | MCP tools (`tests/mcp/`) |


### Filtering

```bash
npm run ctest tests/page/locator-click.spec.ts # Specific file
npm run ctest tests/page/locator-click.spec.ts:12 # Specific location
npm run ctest -- --grep "should click" # By test name
npm run ctest-mcp snapshot # By file name part
```

### Test Directories and Fixtures

| Directory | Import | Key Fixtures | What to Test |
|-----------|--------|--------------|--------------|
| `tests/page/` | `import { test, expect } from './pageTest'` | `page`, `server`, `browserName` | User interactions: click, fill, navigate, locators, assertions |
| `tests/library/` | `import { browserTest, expect } from '../config/browserTest'` | `browser`, `context`, `browserType` | Browser/context lifecycle, cookies, permissions, browser-specific features |
| `tests/playwright-test/` | `import { test, expect } from './playwright-test-fixtures'` | test runner fixtures | Test runner: reporters, config, annotations, retries |
| `tests/mcp/` | `import { test, expect } from './fixtures'` | `client`, `server` | MCP tools via `client.callTool()` |

**Decision rule**: Does the test need `browser`/`browserType`/`context` → `tests/library/`. Just needs `page` + `server` → `tests/page/`.

## DEPS System

Import boundaries are enforced via `DEPS.list` files (52+ across the repo), checked by `npm run flint`.

**Key rule**: Client code NEVER imports server code. Server code NEVER imports client code. Communication is only through the protocol.
When creating or moving files, update the relevant `DEPS.list` to declare allowed imports. Files marked `"strict"` can only import what is explicitly listed.

## Commit Convention

Semantic commit messages: `label(scope): description`

Labels: `fix`, `feat`, `chore`, `docs`, `test`, `devops`

```bash
git checkout -b fix-39562
# ... make changes ...
git add <changed-files>
git commit -m "$(cat <<'EOF'
fix(proxy): handle SOCKS proxy authentication

Fixes: https://github.com/microsoft/playwright/issues/39562
EOF
)"
git push origin fix-39562
gh pr create --repo microsoft/playwright --head username:fix-39562 \
--title "fix(proxy): handle SOCKS proxy authentication" \
--body "$(cat <<'EOF'
## Summary
- <describe the change>

Fixes https://github.com/microsoft/playwright/issues/39562
EOF
)"
```

Branch naming for issue fixes: `fix-<issue-number>`

## Development Guides

Detailed guides for common development tasks:

- **[Architecture: Client, Server, and Dispatchers](.claude/skills/playwright-dev/library.md)** — package layout, protocol layer, ChannelOwner/SdkObject/Dispatcher base classes, DEPS rules, end-to-end RPC flow, object lifecycle
- **[Adding and Modifying APIs](.claude/skills/playwright-dev/api.md)** — 6-step process: define docs → implement client → define protocol → implement dispatcher → implement server → write tests
- **[MCP Tools and CLI Commands](.claude/skills/playwright-dev/mcp-dev.md)** — `defineTool()`/`defineTabTool()`, tool capabilities, CLI `declareCommand()`, config options, testing with MCP fixtures
- **[Vendoring Dependencies](.claude/skills/playwright-dev/vendor.md)** — bundle architecture, esbuild setup, typed wrappers, adding deps to existing bundles
4 changes: 2 additions & 2 deletions packages/devtools/src/grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => {
</div>
{isExpanded && (
<div className='session-chips'>
{entries.map(session => <SessionChip key={session.browserDescriptor.pipeName} descriptor={session.browserDescriptor} wsUrl={session.wsUrl} visible={isExpanded} model={model} />)}
{entries.map(session => <SessionChip key={session.browserDescriptor.guid} descriptor={session.browserDescriptor} wsUrl={session.wsUrl} visible={isExpanded} model={model} />)}
</div>
)}
</div>
Expand All @@ -103,7 +103,7 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => {
};

const SessionChip: React.FC<{ descriptor: BrowserDescriptor; wsUrl: string | undefined; visible: boolean; model: SessionModel }> = ({ descriptor, wsUrl, visible, model }) => {
const href = '#session=' + encodeURIComponent(descriptor.pipeName!);
const href = '#session=' + encodeURIComponent(descriptor.guid);

const channel = React.useMemo(() => {
if (!wsUrl || !visible)
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const App: React.FC = () => {
}, []);

if (socketPath) {
const wsUrl = model.sessionBySocketPath(socketPath)?.wsUrl;
const wsUrl = model.sessionByGuid(socketPath)?.wsUrl;
return <DevTools wsUrl={wsUrl || undefined} />;
}
return <Grid model={model} />;
Expand Down
8 changes: 4 additions & 4 deletions packages/devtools/src/sessionModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ export class SessionModel {
}
}

sessionBySocketPath(socketPath: string): SessionStatus | undefined {
return this.sessions.find(s => s.browserDescriptor.pipeName === socketPath);
sessionByGuid(guid: string): SessionStatus | undefined {
return this.sessions.find(s => s.browserDescriptor.guid === guid);
}

private async _fetchSessions() {
Expand Down Expand Up @@ -103,7 +103,7 @@ export class SessionModel {
await fetch('/api/sessions/close', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ browserDescriptor: descriptor }),
body: JSON.stringify({ sessionGuid: descriptor.guid }),
});
await this._fetchSessions();
}
Expand All @@ -112,7 +112,7 @@ export class SessionModel {
await fetch('/api/sessions/delete-data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ browserDescriptor: descriptor }),
body: JSON.stringify({ sessionGuid: descriptor.guid }),
});
await this._fetchSessions();
}
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/cli/client/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ async function killAllDaemons(): Promise<void> {
async function listSessions(registry: Registry, clientInfo: ClientInfo, all: boolean): Promise<void> {
if (all) {
const entries = registry.entryMap();
const serverEntries = await serverRegistry.list({ gc: true });
const serverEntries = await serverRegistry.list();
if (entries.size === 0 && serverEntries.size === 0) {
console.log('No browsers found.');
return;
Expand Down
12 changes: 6 additions & 6 deletions packages/playwright-core/src/client/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
private _closeReason: string | undefined;
private _harRouters: HarRouter[] = [];
private _onRecorderEventSink: RecorderEventSink | undefined;
private _allowedProtocols: string[] | undefined;
private _disallowedProtocols: string[] | undefined;
private _allowedDirectories: string[] | undefined;

static from(context: channels.BrowserContextChannel): BrowserContext {
Expand Down Expand Up @@ -555,21 +555,21 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
await this._channel.exposeConsoleApi();
}

_setAllowedProtocols(protocols: string[]) {
this._allowedProtocols = protocols;
_setDisallowedProtocols(protocols: string[]) {
this._disallowedProtocols = protocols;
}

_checkUrlAllowed(url: string) {
if (!this._allowedProtocols)
if (!this._disallowedProtocols)
return;
let parsedURL;
try {
parsedURL = new URL(url);
} catch (e) {
throw new Error(`Access to ${url} is blocked. Invalid URL: ${e.message}`);
}
if (!this._allowedProtocols.includes(parsedURL.protocol))
throw new Error(`Access to "${parsedURL.protocol}" URL is blocked. Allowed protocols: ${this._allowedProtocols.join(', ')}. Attempted URL: ${url}`);
if (this._disallowedProtocols.includes(parsedURL.protocol))
throw new Error(`Access to "${parsedURL.protocol}" protocol is blocked. Attempted URL: "${url}"`);
}

_setAllowedDirectories(rootDirectories: string[]) {
Expand Down
Loading
Loading