참고소스 수정본

This commit is contained in:
LASTA_DEV01\lasta
2026-05-12 19:40:31 +09:00
parent 0f34a451fc
commit 2e9204243d
8708 changed files with 3259488 additions and 869 deletions

View File

@@ -0,0 +1,18 @@
---
name: playwright-dev
description: Explains how to develop Playwright - add APIs, MCP tools, CLI commands, and vendor dependencies.
---
# Playwright Development Guide
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](tools.md) — add MCP tools, CLI commands, config options
- [Vendor Dependencies & Bundling](vendor.md) — utilsBundle, coreBundle, babelBundle; adding vendored npm packages; DEPS.list; `check_deps`
- [Updating WebKit Safari Version](webkit-safari-version.md) — update the Safari version string in the WebKit user-agent
- [Bisecting Across Published Versions](bisect-published-versions.md) — reproduce regressions side-by-side from npm and diff `node_modules/playwright/lib/` between versions
- [Dashboard](dashboard.md) - the UI powering the "playwright cli show" command, and how to work on it

View File

@@ -0,0 +1,295 @@
# 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` — always take the 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.
```
Keep methods, events and property definitions sorted alphabetically within the file.
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<XxxChannel>` and call through `this._channel`:
```typescript
// Direct channel call (most common)
async methodName(param: string, options: channels.FrameMethodNameOptions = {}): Promise<void> {
await this._channel.methodName({ param, ...options, timeout: this._timeout(options) });
}
// Channel call with response wrapping
async goto(url: string, options: channels.FrameGotoOptions = {}): Promise<network.Response | null> {
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<void> {
await this._page.methodName(progress, params.value);
}
// With response wrapping
async goto(params: channels.FrameGotoParams, progress: Progress): Promise<channels.FrameGotoResult> {
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<channels.PageExpectScreenshotResult> {
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<channels.FrameQuerySelectorAllResult> {
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<channels.XxxMethodResult>`
- 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<network.Response | null> {
// ... 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)
```

View File

@@ -0,0 +1,71 @@
# Bisecting a Regression Across Published Playwright Versions
When a user reports a regression between two published Playwright versions (e.g. "works in 1.58, broken in 1.59.1"), reproduce both side by side from npm — do **not** try to bisect against the monorepo source. Reading the compiled JS in `node_modules/playwright/lib/**` is faster and avoids build/branch confusion.
## Setup (two side-by-side installs)
Use `~/tmp/<version-tag>/` (NOT `/tmp/`) — the user's shell sessions live in `~/tmp`.
```bash
mkdir -p ~/tmp/<good>/tests ~/tmp/<bad>/tests
# Skip `npm init playwright@latest` — it's interactive and the scaffold
# pulls in 3 projects (chromium/firefox/webkit) which produces 6 test runs
# from a single spec and is confusing. Do this instead:
( cd ~/tmp/<good> && npm init -y && npm install @playwright/test@<good-ver> && npx playwright install chromium)
( cd ~/tmp/<bad> && npm init -y && npm install @playwright/test@<bad-ver> && npx playwright install chromium )
```
Write a **minimal** `playwright.config.ts` with a single chromium project — the default scaffold's 3-project config will run the same spec 6 times and obscure output:
```ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
});
```
Drop the repro spec (and any helper files) into both folders identically. Run:
```bash
( cd ~/tmp/<good> && npx playwright test )
( cd ~/tmp/<bad> && npx playwright test )
```
Confirm the difference is real before investigating.
## Investigating the diff in node_modules
The compiled JS in `node_modules/playwright-core/lib/` and `node_modules/playwright/lib/` is the source of truth for what shipped.
In recent versions of Playwright these are bundled, so you can't compare
on per-file basis. You can extract files from bundles via grep though and compare.
Once you've found a candidate function, diff it across the two versions. Patches are usually 13 lines.
## Verifying the hypothesis
Edit the compiled JS in `~/tmp/<bad>/node_modules/playwright/lib/...` directly and re-run the test. No build step is needed — Node loads the JS as-is. Revert when done (or just delete the folder).
For stack-trace bugs in particular, a `console.log(new Error().stack)` inserted at the capture site (e.g. inside `expect.js`'s `captureRawStack`) instantly shows whether the issue is microtask-boundary related vs. a stack-filter regression vs. something else.
## Reporting
When the root cause is confirmed:
1. Quote the offending lines from `node_modules/.../lib/...` of the **bad** version, with file path.
2. Show the equivalent code from the **good** version for contrast.
3. Explain *why* the change breaks the user's case (don't just point at the diff).
4. Propose and verify a minimal fix by patching the bad install in place.
Post the writeup as a comment on the original issue with `gh issue comment <number> --repo microsoft/playwright --body "$(cat <<'EOF' ... EOF)"`.
## Pitfalls
- **Don't run `npm init playwright@latest`** — it's interactive and `--quiet` does not skip the prompts. `npm init -y` + `npm install @playwright/test@<ver>` is faster and deterministic.
- **Don't use the scaffold's default config** — the 3 browser projects multiply test runs by 3 and confuse the output. One chromium project is enough for 99% of repros.
- **Don't `cd` between commands in a single Bash call without `&&`** — the shell cwd resets between tool invocations.
- **`/tmp/` is not `~/tmp/`** — pick one and stay consistent. The user's interactive shells default to `~/tmp/`, so prefer that.
- **Don't `rm -rf` an existing `~/tmp/<ver>/`** without checking — it may be the user's prior work. Edit in place instead.
- **Don't try to map the bug to monorepo source first.** The shipped JS is what the user is running; source may have already been refactored or fixed on `main`. Investigate `node_modules/` first, then map the fix back to source only when proposing the upstream patch.

View File

@@ -0,0 +1,35 @@
# Developing Dashboard
`packages/dashboard` contains the sourcecode behind `playwright cli show`,
a dashboard that allow supervising agents while they use playwright cli.
Important code paths:
- `packages/dashboard` has the UI
- `dashboardController.ts` has the backend
- `show` section in `cli-client/program.ts`
You can use Playwright CLI to look at the dashboard:
```bash
# start the dashboard server in the background
npx playwright cli show --port=0
# open it with Playwright CLI
npx playwright cli open --session=dashboard localhost:PORT
npx playwright cli snapshot
# take screenshots to look at UI stuff
npx playwright cli screenshot
# take videos to showcase you work!
npx playwright cli video-start video.webm
# chapters are not everything - look at video-recording.md to learn about overlays, much more powerful! embrace creativity.
npx playwright cli video-chapter "Chapter Title" --description="Details" --duration=2000
npx playwright cli video-stop
# afterwards, use ffmpeg to turn the video into mp4 for sharing.
```
Full CLI reference: `packages/playwright-core/src/tools/cli-client/skill/SKILL.md`. In this repo, invoke as `npx playwright cli` instead of `playwright-cli`.

View File

@@ -0,0 +1,418 @@
# Playwright Library Architecture: Client, Server, and Dispatchers
Playwright uses a client-server architecture connected by a protocol layer. The client provides the public API, the server performs actual browser automation, and dispatchers bridge the two over an RPC channel.
## Package Layout
```
packages/protocol/src/
protocol.yml — RPC protocol definition (source of truth)
channels.d.ts — generated TypeScript channel interfaces
callMetadata.d.ts — call metadata types
packages/playwright-core/src/
client/ — public API objects (ChannelOwner subclasses)
server/ — browser automation implementation (SdkObject subclasses)
server/dispatchers/ — protocol bridge (Dispatcher subclasses)
protocol/ — validators (generated + primitives)
utils/isomorphic/ — shared code used by both client and server
protocolMetainfo.ts — generated method metadata (flags, titles)
```
## Dependency Rules (DEPS.list)
Each directory has a `DEPS.list` constraining its imports. These are enforced by `npm run flint`.
Entries can be relative paths, alias paths (`@isomorphic/**`, `@utils/**`), or `node_modules/<pkg>` to allow a specific npm package import. The `"strict"` marker disables inheritance from parent folders. Section headers like `[filename.ts]` scope rules to a single file.
**client/** can import from:
- `../protocol/` — validators and channel types
- `../utils/isomorphic` — shared utilities
**server/** can import from:
- `../protocol/`, `../utils/`, `../utils/isomorphic/`, `../utilsBundle.ts`
- `./` (own directory), `./codegen/`, `./isomorphic/`, `./har/`, `./recorder/`, `./registry/`, `./utils/`
- Only `playwright.ts` can import browser engines (`./chromium/`, `./firefox/`, `./webkit/`, `./bidi/`, `./android/`, `./electron/`)
- Only `devtoolsController.ts` can additionally import `./chromium/`
**server/dispatchers/** can import from:
- `../../protocol/`, `../../utils/`, `../../utils/isomorphic/`
- `../**` — all server modules
**Key rule:** Client code NEVER imports server code. Server code NEVER imports client code. They communicate only through the protocol.
**Vendored npm packages** (anything under `node_modules/`) go through `src/utilsBundle.ts` — a single bundled file that re-exports the vendored symbols. Adding a new dep or changing a DEPS.list entry for vendored code: see [vendor.md](vendor.md).
## Protocol Layer
### protocol.yml
Defines all RPC interfaces, commands (methods), events, and types. Example:
```yaml
Page:
type: interface
extends: EventTarget
initializer:
mainFrame: Frame
viewportSize: { type: object?, properties: { width: int, height: int } }
commands:
goto:
parameters:
url: string
timeout: float
waitUntil: LifecycleEvent?
returns:
response: Response?
events:
close: {}
navigated:
url: string
name: string
```
### Code Generation
Running `node utils/generate_channels.js` (or via watch) produces:
- `packages/protocol/src/channels.d.ts` — TypeScript types: `PageChannel`, `PageGotoParams`, `PageGotoResult`, `PageInitializer`, event types
- `packages/playwright-core/src/protocol/validator.ts` — runtime validators: `scheme.PageGotoParams = tObject({...})`
- `packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts` — method flags (slowMo, snapshot, etc.)
### Wire Format
```
Client → Server (RPC call): { id, guid, method, params, metadata? }
Server → Client (response): { id, result } or { id, error, log? }
Server → Client (event): { guid, method, params }
Server → Client (lifecycle): { guid, method: '__create__'|'__adopt__'|'__dispose__', params }
```
Object references are serialized as `{ guid: "object-guid" }` and resolved by validators.
## Client Layer
### ChannelOwner — Base Class
Every client-side API object (Page, Frame, Browser, etc.) extends `ChannelOwner<T>`:
```
packages/playwright-core/src/client/channelOwner.ts
```
Key properties:
- `_connection: Connection` — the RPC connection
- `_channel: T` — Proxy that intercepts method calls and sends RPC messages
- `_guid: string` — unique identifier matching the server-side object
- `_type: string` — type name (e.g., 'Page', 'Frame')
- `_parent: ChannelOwner` — parent in the object tree
- `_objects: Map<string, ChannelOwner>` — child objects
- `_initializer` — initial state received from server on creation
How `_channel` works: It's a Proxy. When you call `this._channel.goto(params)`:
1. Proxy intercepts the `goto` property access
2. Finds the validator for `PageGotoParams`
3. Returns an async function that validates params, wraps in `_wrapApiCall`, and calls `_connection.sendMessageToServer()`
Event subscription optimization: `_eventToSubscriptionMapping` maps JS event names to protocol subscription events. When the first listener is added, calls `updateSubscription(event, true)` on the channel. When last listener is removed, calls `updateSubscription(event, false)`. This way the server only sends events that have listeners.
### Connection
```
packages/playwright-core/src/client/connection.ts
```
Manages the client-server transport:
- `_objects: Map<string, ChannelOwner>` — all live remote objects by GUID
- `_callbacks: Map<number, {resolve, reject}>` — pending RPC calls by message ID
- `sendMessageToServer(object, method, params, apiZone)` — sends RPC call, returns promise
- `dispatch(message)` — handles incoming messages:
- Response (has `id`): resolves/rejects the matching callback
- `__create__`: instantiates ChannelOwner subclass via factory switch
- `__adopt__`: reparents a child object
- `__dispose__`: disposes object and all children
- Event (has `method`): emits on the object's `_channel`
### Representative Client Classes
| Class | File | Key delegation |
|-------|------|----------------|
| `Playwright` | `playwright.ts` | Root object; owns `chromium`, `firefox`, `webkit` BrowserTypes |
| `BrowserType` | `browserType.ts` | `launch()``_channel.launch()` |
| `Browser` | `browser.ts` | `newContext()``_channel.newContext()` |
| `BrowserContext` | `browserContext.ts` | Owns pages, routes, tracing, cookies |
| `Page` | `page.ts` | Delegates most calls to `_mainFrame`; owns keyboard/mouse/touchscreen |
| `Frame` | `frame.ts` | `goto()`, `click()`, `evaluate()``_channel.*` |
| `Locator` | `locator.ts` | Delegates to `Frame` methods with selector + `strict: true` |
| `ElementHandle` | `elementHandle.ts` | DOM element reference |
## Server Layer
### SdkObject — Base Class
Every server-side domain object extends `SdkObject`:
```
packages/playwright-core/src/server/instrumentation.ts
```
Key properties:
- `guid: string` — unique identifier (shared with client-side ChannelOwner)
- `attribution: Attribution` — ownership chain: `{ playwright, browserType?, browser?, context?, page?, frame? }`
- `instrumentation: Instrumentation` — hooks for tracing, debugging, test runner integration
Attribution is inherited from parent on construction. Instrumentation hooks include:
`onBeforeCall`, `onAfterCall`, `onBeforeInputAction`, `onCallLog`, `onPageOpen/Close`, `onBrowserOpen/Close`, `onDialog`, `onDownload`.
### Key Server Classes
| Class | File | Purpose |
|-------|------|---------|
| `Playwright` | `playwright.ts` | Root entry point; creates BrowserTypes |
| `BrowserType` | `browserType.ts` | Launches browser processes |
| `Browser` | `browser.ts` | Abstract base; owns BrowserContexts |
| `BrowserContext` | `browserContext.ts` | Isolation boundary; owns pages, cookies, routes |
| `Page` | `page.ts` | Owns FrameManager, workers; delegates to `PageDelegate` |
| `FrameManager` | `frames.ts` | Manages frame hierarchy |
| `Frame` | `frames.ts` | Navigation, DOM queries, JavaScript evaluation |
| `ElementHandle` | `dom.ts` | DOM element operations |
| `ProgressController` | `progress.ts` | Wraps async operations with timeout/cancellation/logging |
### PageDelegate Pattern
`Page` delegates browser-specific operations to a `PageDelegate` interface:
```typescript
interface PageDelegate {
navigateFrame(frame, url, referer): Promise<GotoResult>;
takeScreenshot(progress, format, ...): Promise<Buffer>;
adoptElementHandle(handle, to): Promise<ElementHandle>;
// ... more browser-specific operations
}
```
Implementations:
- `packages/playwright-core/src/server/chromium/crPage.ts` — uses CDP
- `packages/playwright-core/src/server/firefox/ffPage.ts`
- `packages/playwright-core/src/server/webkit/wkPage.ts`
### Browser Engine Directories
| Directory | Protocol | Key files |
|-----------|----------|-----------|
| `chromium/` | Chrome DevTools Protocol (CDP) | `crBrowser.ts`, `crPage.ts`, `crConnection.ts` |
| `firefox/` | Firefox internal protocol | `ffBrowser.ts`, `ffPage.ts`, `ffConnection.ts` |
| `webkit/` | WebKit internal protocol | `wkBrowser.ts`, `wkPage.ts`, `wkConnection.ts` |
| `bidi/` | WebDriver BiDi | `bidiChromium.ts`, `bidiFirefox.ts` |
| `android/` | ADB | `android.ts` |
| `electron/` | Electron/CDP | `electron.ts` |
## Dispatcher Layer
Dispathers do not implement things, they translate protocol to the server code calls.
### Dispatcher — Base Class
```
packages/playwright-core/src/server/dispatchers/dispatcher.ts
```
Dispatchers bridge server objects to the protocol. Each wraps an `SdkObject` and exposes methods matching the protocol channel.
```typescript
class Dispatcher<Type extends SdkObject, ChannelType, ParentScopeType extends DispatcherScope>
```
Key properties:
- `connection: DispatcherConnection` — the server-side connection
- `_object: Type` — the wrapped server object
- `_guid: string` — same GUID as the server object
- `_type: string` — type name matching protocol
- `_parent: ParentScopeType` — parent dispatcher
- `_dispatchers: Map<string, DispatcherScope>` — child dispatchers
Key methods:
- `_dispatchEvent(method, params)` — sends event to client via `connection.sendEvent()`
- `_runCommand(callMetadata, method, params)` — wraps method call in `ProgressController`, calls `this[method](params, progress)`
- `_dispose()` — recursively disposes self and children, sends `__dispose__` to client
- `adopt(child)` — reparents child dispatcher, sends `__adopt__` to client
- `addObjectListener(event, handler)` — listens on wrapped server object, auto-cleaned on dispose
### Dispatcher Creation Pattern
Dispatchers use a static factory to ensure one-dispatcher-per-object:
```typescript
static from(parentScope, object): XxxDispatcher {
return parentScope.connection.existingDispatcher<XxxDispatcher>(object) || new XxxDispatcher(parentScope, object);
}
```
The constructor sends `__create__` to the client with the initializer data.
### DispatcherConnection
Server-side counterpart to client's `Connection`:
- `_dispatcherByGuid` — all dispatchers by GUID
- `_dispatcherByObject` — maps server objects to their dispatchers (ensures 1:1)
- `dispatch(message)` — validates params, creates `CallMetadata`, calls instrumentation hooks, runs dispatcher method, validates result, sends response
- `sendCreate/sendAdopt/sendDispose/sendEvent` — lifecycle messages to client
- GC: buckets with limits (JSHandle/ElementHandle: 100k, others: 10k); oldest 10% disposed when exceeded
### Dispatcher Hierarchy
```
RootDispatcher
└── PlaywrightDispatcher
├── BrowserTypeDispatcher (per engine)
│ └── BrowserDispatcher
│ └── BrowserContextDispatcher
│ ├── PageDispatcher
│ │ ├── FrameDispatcher (main + child frames)
│ │ ├── WorkerDispatcher
│ │ └── ...
│ ├── TracingDispatcher
│ └── APIRequestContextDispatcher
├── AndroidDispatcher
├── ElectronDispatcher
└── LocalUtilsDispatcher
```
### Key Dispatcher Files
| File | Dispatches for |
|------|---------------|
| `playwrightDispatcher.ts` | Playwright, BrowserType registration |
| `browserTypeDispatcher.ts` | BrowserType (launch, connect) |
| `browserDispatcher.ts` | Browser |
| `browserContextDispatcher.ts` | BrowserContext |
| `pageDispatcher.ts` | Page, Worker, BindingCall |
| `frameDispatcher.ts` | Frame |
| `networkDispatchers.ts` | Request, Response, Route, WebSocket, APIRequestContext |
| `elementHandlerDispatcher.ts` | ElementHandle |
| `jsHandleDispatcher.ts` | JSHandle |
| `dialogDispatcher.ts` | Dialog |
| `tracingDispatcher.ts` | Tracing |
| `artifactDispatcher.ts` | Artifact |
## End-to-End Flow Example
`await page.goto('https://example.com')`:
```
CLIENT:
Page.goto()
→ _wrapApiCall() captures stack trace, creates ApiZone
→ _channel.goto({ url, timeout })
→ Proxy validates PageGotoParams
→ connection.sendMessageToServer(page, 'goto', params)
→ sends { id: 1, guid: 'page@abc', method: 'goto', params: {...} }
→ waits on callback promise
SERVER:
DispatcherConnection.dispatch(message)
→ validates PageGotoParams (wire → objects)
→ creates CallMetadata
→ instrumentation.onBeforeCall()
→ PageDispatcher._runCommand('goto', params)
→ ProgressController.run(progress => this.goto(params, progress))
→ PageDispatcher.goto(): this._object.mainFrame().goto(progress, url, params)
→ Frame.goto() → PageDelegate.navigateFrame() → CDP/protocol call
→ validates PageGotoResult (objects → wire)
→ instrumentation.onAfterCall()
→ sends { id: 1, result: { response: { guid: 'response@xyz' } } }
CLIENT:
connection.dispatch(response)
→ validates PageGotoResult (wire → objects)
→ resolves callback promise
→ _wrapApiCall completes, returns Response object
```
## Object Lifecycle
1. **Creation**: Server creates SdkObject → dispatcher constructor sends `__create__` → client `Connection.dispatch()` instantiates `ChannelOwner` subclass
2. **Adoption**: `dispatcher.adopt(child)` sends `__adopt__` → client reparents the `ChannelOwner`
3. **Disposal**: `dispatcher._dispose()` recursively disposes children → sends `__dispose__` → client removes `ChannelOwner` from maps
4. **GC**: Server-side `maybeDisposeStaleDispatchers()` evicts oldest dispatchers per bucket when limits exceeded
## Testing: tests/library vs tests/page
Tests live in two directories under `tests/`, each with distinct scope and fixtures.
### tests/library — API and Feature Tests
Tests the **Playwright public API surface**, browser lifecycle, and feature-level behavior. Uses `browserTest` fixtures which provide direct access to `browser`, `browserType`, `context`, and `contextFactory`.
```typescript
import { browserTest as test, expect } from '../config/browserTest';
test('should create new page', async ({ browser }) => {
const page = await browser.newPage();
expect(browser.contexts().length).toBe(1);
await page.close();
});
```
**What belongs here:**
- Browser and BrowserType API (`launch`, `connect`, `version`, `newContext`)
- BrowserContext API (cookies, storage state, permissions, proxy, CSP, geolocation, network interception at context level)
- Browser-specific features (`chromium/` for CDP, tracing, extensions, JS/CSS coverage, OOPIF; `firefox/` for launcher specifics)
- Protocol and channel tests
- Inspector, codegen, and recorder features (`inspector/`)
- Event system tests (`events/`)
- Unit tests for internal utilities (`unit/`)
**Key fixtures** (from `browserTest`): `browser`, `browserType`, `context`, `contextFactory`, `launchPersistent`, `createUserDataDir`, `startRemoteServer`, `pageWithHar`.
### tests/page — Page Interaction Tests
Tests **user-facing page interactions**: clicking, typing, navigation, locators, assertions, and DOM operations. Uses `pageTest` fixtures which provide a ready-to-use `page` plus test servers.
```typescript
import { test as it, expect } from './pageTest';
it('should click button', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.locator('button').click();
expect(await page.evaluate(() => window['result'])).toBe('Clicked');
});
```
**What belongs here:**
- Locator API (click, fill, type, select, query, filtering, convenience methods)
- ElementHandle interactions (click, screenshot, selection, bounding box)
- Expect/assertion matchers (boolean, text, value, accessibility)
- Page navigation (`goto`, `waitForNavigation`, `waitForURL`)
- Frame evaluation and hierarchy
- Request/response interception at page level
- JSHandle operations
- Screenshot and visual comparison tests
**Key fixtures** (from `pageTest`/`serverFixtures`): `page`, `server`, `httpsServer`, `proxyServer`, `asset`.
### Decision Rule
| Question | → Directory |
|----------|-------------|
| Does it test browser/context lifecycle or launch options? | `tests/library` |
| Does it test a browser-specific protocol feature (CDP, etc.)? | `tests/library` |
| Does it test user interaction with page content (click, type, assert)? | `tests/page` |
| Does it test locators, selectors, or DOM queries? | `tests/page` |
| Does the test need direct `browser` or `browserType` access? | `tests/library` |
| Does the test just need a `page` and a test server? | `tests/page` |
### Running Tests
- `npm run ctest <file>` — runs on Chromium only (fast, use during development)
- `npm run test <file>` — runs on all browsers (Chromium, Firefox, WebKit)
Examples:
```bash
npm run ctest tests/library/browser-context-cookies.spec.ts
npm run ctest tests/page/locator-click.spec.ts
npm run test tests/library/browser-context-cookies.spec.ts
```
### Configuration
Both directories share a single config at `tests/library/playwright.config.ts`. It creates separate projects (`{browserName}-library` and `{browserName}-page`) pointing to their respective `testDir`.

View File

@@ -0,0 +1,512 @@
# MCP Tools and CLI Commands
## Adding MCP Tools
### Step 1: Create the Tool File
Create `packages/playwright-core/src/tools/backend/<your-tool>.ts`.
Import zod from the MCP bundle and use `defineTool` or `defineTabTool`:
```typescript
import { z } from '../../zodBundle';
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-core/src/tools/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-core/src/tools/backend/tools.ts`:
```typescript
import myTool from './myTool';
export const browserTools: Tool<any>[] = [
// ... existing tools ...
...myTool,
];
```
### Step 4: Write Tests
Create `tests/mcp/<category>.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 <category>`
- 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-core/src/tools/cli-daemon/commands.ts`, use `declareCommand()`:
```typescript
import { z } from '../../zodBundle';
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-core/src/tools/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-core/src/tools/cli-daemon/command.ts`
2. Add it to the `categories` array in `packages/playwright-core/src/tools/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-<category>.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-<category>`
- 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-core/src/tools/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-core/src/tools/mcp/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-core/src/tools/mcp/program.ts`
Add CLI flag:
```typescript
command
.option('--my-option <value>', '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-core/src/tools/
├── backend/ # All MCP tool implementations
│ ├── tool.ts # Tool/TabTool types, defineTool(), defineTabTool()
│ ├── tools.ts # Tool registry (browserTools array, filteredTools)
│ ├── browserBackend.ts # Browser backend
│ ├── context.ts # Browser context management
│ ├── tab.ts # Tab management
│ ├── response.ts # Response class, parseResponse()
│ ├── 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
│ └── utils.ts # shared utilities
├── mcp/ # MCP server
│ ├── config.d.ts # Config type, ToolCapability type
│ ├── config.ts # Config resolution, CLIOptions, FullConfig
│ ├── program.ts # MCP server CLI setup
│ ├── index.ts # MCP server entry
│ ├── browserFactory.ts # Browser factory
│ ├── extensionContextFactory.ts
│ ├── cdpRelay.ts # CDP relay
│ ├── watchdog.ts # Watchdog
│ └── log.ts # Logging
├── cli-client/ # CLI client
│ ├── program.ts # CLI client entry (argument parsing)
│ ├── session.ts # Session management
│ └── registry.ts # Session registry
├── cli-daemon/ # CLI 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
│ └── program.ts # Daemon program entry
├── dashboard/ # Dashboard UI
│ ├── dashboardApp.ts # Dashboard app
│ └── dashboardController.ts
├── utils/
│ ├── socketConnection.ts # Socket connection utilities
│ └── mcp/ # MCP SDK utilities
│ ├── server.ts # MCP server wrapper
│ ├── tool.ts # ToolSchema type, toMcpTool()
│ └── http.ts # HTTP utilities
└── exports.ts # Public exports
packages/playwright/src/
└── 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)
├── <category>.spec.ts # MCP tool tests
└── cli-<category>.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
```

View File

@@ -0,0 +1,929 @@
# Playwright Trace System - Comprehensive Guide
## 1. Overview
The Playwright trace system is a comprehensive recording and visualization framework that captures:
- **Actions** (API calls, user interactions)
- **Network traffic** (HAR format)
- **Snapshots** (DOM snapshots at key moments)
- **Screencast frames** (video of page rendering)
- **Console messages** and events
- **Errors** and logs
- **Resources** (images, stylesheets, scripts, etc.)
---
## 2. File Structure
### packages/trace/src/ - Trace Type Definitions
Located in `/home/pfeldman/code/playwright/packages/trace/src/`
**Key Files:**
- **trace.ts** - Core trace event type definitions
- **har.ts** - HTTP Archive format (network traffic)
- **snapshot.ts** - DOM snapshot data structures
- **DEPS.list** - Dependencies marker
**File List:**
```
trace/src/
├── trace.ts (183 lines) - Main trace event types
├── har.ts (189 lines) - HAR format types
├── snapshot.ts (62 lines) - Snapshot data structures
└── DEPS.list - Dependencies file
```
---
## 3. Trace Event Types (trace.ts)
### 3.1 Core Event Types
**VERSION: 8** (Current format version)
#### ContextCreatedTraceEvent
```typescript
type ContextCreatedTraceEvent = {
version: number,
type: 'context-options',
origin: 'testRunner' | 'library',
browserName: string,
channel?: string,
platform: string,
playwrightVersion?: string,
wallTime: number, // Milliseconds since epoch
monotonicTime: number, // Internal monotonic clock
title?: string,
options: BrowserContextEventOptions,
sdkLanguage?: Language,
testIdAttributeName?: string,
contextId?: string,
testTimeout?: number,
};
```
#### BeforeActionTraceEvent
Emitted when an action starts:
```typescript
type BeforeActionTraceEvent = {
type: 'before',
callId: string, // Unique action identifier
startTime: number, // Monotonic time when action started
title?: string, // User-facing action name
class: string, // API class (e.g., 'Page', 'Frame')
method: string, // API method (e.g., 'click', 'goto')
params: Record<string, any>, // Method parameters
stepId?: string, // Test step identifier
beforeSnapshot?: string, // "before@<callId>"
stack?: StackFrame[], // Call stack
pageId?: string, // Associated page ID
parentId?: string, // Parent action (for nested actions)
group?: string, // Action group (e.g., 'wait', 'click')
};
```
#### InputActionTraceEvent
For input/pointer interactions:
```typescript
type InputActionTraceEvent = {
type: 'input',
callId: string,
inputSnapshot?: string, // "input@<callId>"
point?: Point, // Mouse/pointer coordinates
};
```
#### AfterActionTraceEvent
Emitted when an action completes:
```typescript
type AfterActionTraceEvent = {
type: 'after',
callId: string,
endTime: number, // Monotonic time when action ended
afterSnapshot?: string, // "after@<callId>"
error?: SerializedError, // Error if action failed
attachments?: AfterActionTraceEventAttachment[], // Files, screenshots
annotations?: AfterActionTraceEventAnnotation[], // Custom annotations
result?: any, // Return value
point?: Point, // Final pointer position
};
```
#### ActionTraceEvent (Composite)
Combines before, after, and input events:
```typescript
type ActionTraceEvent = {
type: 'action',
} & Omit<BeforeActionTraceEvent, 'type'>
& Omit<AfterActionTraceEvent, 'type'>
& Omit<InputActionTraceEvent, 'type'>;
```
#### Other Event Types
**ScreencastFrameTraceEvent** - Video frame data
```typescript
type ScreencastFrameTraceEvent = {
type: 'screencast-frame',
pageId: string,
sha1: string, // Resource SHA1
width: number, // Frame width
height: number, // Frame height
timestamp: number, // Frame timestamp
frameSwapWallTime?: number,
};
```
**EventTraceEvent** - Browser events (dialog, navigation, etc.)
```typescript
type EventTraceEvent = {
type: 'event',
time: number,
class: string, // Event source class
method: string, // Event method
params: any, // Event parameters
pageId?: string,
};
```
**ConsoleMessageTraceEvent** - Console output
```typescript
type ConsoleMessageTraceEvent = {
type: 'console',
time: number,
pageId?: string,
messageType: string, // 'log', 'error', 'warn', etc.
text: string,
args?: { preview: string, value: any }[],
location: { url: string, lineNumber: number, columnNumber: number },
};
```
**LogTraceEvent** - Action logs
```typescript
type LogTraceEvent = {
type: 'log',
callId: string,
time: number,
message: string,
};
```
**ResourceSnapshotTraceEvent** - Network request
```typescript
type ResourceSnapshotTraceEvent = {
type: 'resource-snapshot',
snapshot: ResourceSnapshot, // HAR Entry
};
```
**FrameSnapshotTraceEvent** - DOM snapshot
```typescript
type FrameSnapshotTraceEvent = {
type: 'frame-snapshot',
snapshot: FrameSnapshot,
};
```
**StdioTraceEvent** - Process output (stdout/stderr)
```typescript
type StdioTraceEvent = {
type: 'stdout' | 'stderr',
timestamp: number,
text?: string,
base64?: string, // Binary output
};
```
**ErrorTraceEvent** - Unhandled errors
```typescript
type ErrorTraceEvent = {
type: 'error',
message: string,
stack?: StackFrame[],
};
```
---
## 4. HAR Format (har.ts)
Follows HTTP Archive 1.2 specification. Key structure for network traffic:
```typescript
type HARFile = {
log: Log,
};
type Log = {
version: string,
creator: Creator,
browser?: Browser,
pages?: Page[],
entries: Entry[], // Network requests
};
type Entry = {
pageref?: string,
startedDateTime: string,
time: number, // Total time (ms)
request: Request,
response: Response,
cache: Cache,
timings: Timings,
serverIPAddress?: string,
connection?: string,
// Custom Playwright fields:
_frameref?: string,
_monotonicTime?: number,
_serverPort?: number,
_securityDetails?: SecurityDetails,
_wasAborted?: boolean,
_wasFulfilled?: boolean,
_wasContinued?: boolean,
_apiRequest?: boolean, // True for fetch/axios requests
};
```
---
## 5. Snapshot Format (snapshot.ts)
### FrameSnapshot
```typescript
type FrameSnapshot = {
snapshotName?: string,
callId: string, // Associated action
pageId: string,
frameId: string,
frameUrl: string,
timestamp: number,
wallTime?: number,
collectionTime: number, // Time to capture
doctype?: string,
html: NodeSnapshot, // Encoded DOM tree
resourceOverrides: ResourceOverride[], // Inlined resources
viewport: { width: number, height: number },
isMainFrame: boolean,
};
```
### NodeSnapshot
Compact encoding of DOM tree:
```typescript
type NodeSnapshot =
TextNodeSnapshot | // string
SubtreeReferenceSnapshot | // [ [snapshotIndex, nodeIndex] ]
NodeNameAttributesChildNodesSnapshot; // [ name, attributes?, ...children ]
```
### ResourceOverride
Embeds resource data in snapshot:
```typescript
type ResourceOverride = {
url: string,
sha1?: string, // External resource SHA1
ref?: number // Snapshot index reference
};
```
---
## 6. Trace Storage Format
### File Structure
When a trace is recorded, it creates this structure in the traces directory:
```
traces-dir/
├── <traceName>.trace # Main events (JSONL format)
├── <traceName>.network # Network events (JSONL format)
├── <traceName>-chunk1.trace # Additional chunks (if multiple)
├── <traceName>.stacks # Stack trace metadata (optional)
└── resources/
├── <sha1> # Resource files (images, etc.)
└── <sha1>
```
### File Formats
- **`.trace` and `.network`**: JSONL (JSON Lines) - one event per line
- **`.zip`**: Optional archive containing all above files
- **`resources/`**: Binary blobs indexed by SHA1 hash
### Live Trace Format
For live tracing (test runner):
```
traces-dir/
├── <testName>.json # Synthesized trace metadata
├── <testName>/
├── events.jsonl
├── network.jsonl
└── resources/
```
---
## 7. Trace Recording (tracing.ts)
Located: `/home/pfeldman/code/playwright/packages/playwright-core/src/server/trace/recorder/tracing.ts`
### Tracing Class Architecture
```typescript
export class Tracing extends SdkObject implements
InstrumentationListener,
SnapshotterDelegate,
HarTracerDelegate {
// Recording state
private _state: RecordingState;
// Components
private _snapshotter?: Snapshotter; // Captures DOM snapshots
private _harTracer: HarTracer; // Records network requests
private _screencastListeners: ... // Video recording
// Methods
start(options: TracerOptions);
startChunk(progress, options);
stopChunk(progress, params);
stop(progress);
}
```
### What Gets Recorded
**1. Before Action (`onBeforeCall`)**
- Action metadata: class, method, parameters
- Stack trace
- "before" DOM snapshot
- Associated page/frame IDs
**2. Input Actions (`onBeforeInputAction`)**
- Pointer coordinates
- Input type
- Snapshot of input
**3. Action Logs (`onCallLog`)**
- API log messages
- User-facing messages
**4. After Action (`onAfterCall`)**
- Execution time
- Return value
- Error information (if failed)
- "after" DOM snapshot
- Attachments (screenshots, files)
- Annotations (custom data)
**5. Network Traffic (`onEntryFinished`)**
- HTTP request/response details
- Headers, cookies, body
- Timing information
- Security details
**6. Snapshots (`onFrameSnapshot`, `onSnapshotterBlob`)**
- Full DOM tree with inlined resources
- Viewport size
- Resource references
**7. Console Messages (`onConsoleMessage`)**
- Message type (log, error, warn)
- Text content
- Arguments
- Source location
**8. Events**
- Dialogs
- Page errors
- Navigation events
- Downloads
**9. Screencast Frames**
- Video frames (if screenshots enabled)
- Frame dimensions
- Timestamps
**10. Stdio/Errors**
- stdout/stderr output
- Unhandled errors
- Process events
### Recording State
```typescript
type RecordingState = {
options: TracerOptions,
traceName: string,
networkFile: string,
traceFile: string,
tracesDir: string,
resourcesDir: string,
chunkOrdinal: number,
networkSha1s: Set<string>,
traceSha1s: Set<string>,
recording: boolean,
callIds: Set<string>,
groupStack: string[], // For nested groups
};
```
---
## 8. Trace Loading (traceLoader.ts)
Located: `/home/pfeldman/code/playwright/packages/playwright-core/src/utils/isomorphic/trace/traceLoader.ts`
### TraceLoaderBackend Interface
```typescript
interface TraceLoaderBackend {
entryNames(): Promise<string[]>; // List files in trace
hasEntry(entryName: string): Promise<boolean>;
readText(entryName: string): Promise<string | undefined>; // For JSONL
readBlob(entryName: string): Promise<Blob | undefined>; // For resources
isLive(): boolean; // Is this a live/developing trace?
}
```
### Built-in Backends
**ZipTraceLoaderBackend** (traceParser.ts)
- Loads `.trace.zip` files
- Uses ZipFile utility to read entries
- Converts file paths to file:// URLs
### Load Process
```typescript
async load(backend: TraceLoaderBackend, unzipProgress) {
1. Find .trace files (ordinals: "0", "1", etc.)
2. For each ordinal:
a. Read ordinal.trace (events)
b. Read ordinal.network (network events)
c. Parse with TraceModernizer
d. Read ordinal.stacks (if exists)
e. Sort actions by startTime
3. Terminate incomplete actions
4. Finalize snapshot storage
5. Build resource content-type map
6. Push ContextEntry to contextEntries[]
}
```
### Output: ContextEntry[]
```typescript
type ContextEntry = {
origin: 'testRunner' | 'library',
startTime: number, // Min action startTime
endTime: number, // Max action endTime
browserName: string,
wallTime: number,
sdkLanguage?: Language,
testIdAttributeName?: string,
title?: string,
options: BrowserContextEventOptions,
pages: PageEntry[], // Screencast data
resources: ResourceSnapshot[], // HAR entries
actions: ActionEntry[], // Merged before/after events
events: EventTraceEvent[],
stdio: StdioTraceEvent[],
errors: ErrorTraceEvent[],
hasSource: boolean,
contextId: string,
testTimeout?: number,
};
```
---
## 9. Trace Model (traceModel.ts)
Located: `/home/pfeldman/code/playwright/packages/playwright-core/src/utils/isomorphic/trace/traceModel.ts`
### TraceModel Class
High-level data model for trace viewer:
```typescript
class TraceModel {
// Metadata
startTime: number;
endTime: number;
browserName: string;
channel?: string;
platform?: string;
playwrightVersion?: string;
wallTime?: number;
title?: string;
options: BrowserContextEventOptions;
sdkLanguage: Language;
testIdAttributeName?: string;
traceUri: string; // URL to trace
testTimeout?: number;
// Data arrays
pages: PageEntry[]; // Page screencast data
actions: ActionTraceEventInContext[]; // All recorded actions
attachments: Attachment[]; // Screenshots, files
visibleAttachments: Attachment[]; // Non-private attachments
events: (EventTraceEvent | ConsoleMessageTraceEvent)[];
stdio: StdioTraceEvent[];
errors: ErrorTraceEvent[];
resources: ResourceEntry[]; // Network resources
sources: Map<string, SourceModel>; // Source code
errorDescriptors: ErrorDescription[]; // Parsed errors
// Counters
actionCounters: Map<string, number>; // Actions per group
hasSource: boolean; // Has source code available
hasStepData: boolean; // Has test runner data
// Methods
createRelativeUrl(path: string): string;
failedAction(): ActionTraceEventInContext;
filteredActions(actionsFilter: ActionGroup[]): ActionTraceEventInContext[];
}
```
### ActionTraceEventInContext
```typescript
type ActionTraceEventInContext = ActionEntry & {
context: ContextEntry,
group?: ActionGroup, // Added by TraceModel
log: { time: number, message: string }[],
};
```
---
## 10. Trace Modernizer (traceModernizer.ts)
Located: `/home/pfeldman/code/playwright/packages/playwright-core/src/utils/isomorphic/trace/traceModernizer.ts`
### Version Support
- **Latest:** Version 8
- **Supported:** Versions 3-8
- Upgrades older traces to current format
### TraceModernizer Class
```typescript
class TraceModernizer {
constructor(contextEntry: ContextEntry, snapshotStorage: SnapshotStorage);
appendTrace(trace: string); // Parse JSONL trace lines
actions(): ActionEntry[]; // Get parsed actions
private _modernize(event: any); // Upgrade event to latest version
private _innerAppendEvent(event: TraceEvent); // Process event
}
```
### How It Works
1. Parses JSONL (one JSON object per line)
2. Detects trace version from first `context-options` event
3. Applies version-specific upgrades using `_modernize_N_to_N+1()` functions
4. Consolidates before/after events into unified actions
5. Builds dependency graph for nested actions
6. Stores snapshots in SnapshotStorage
---
## 11. Trace Viewer
Located: `/home/pfeldman/code/playwright/packages/trace-viewer/src/`
### Structure
```
trace-viewer/src/
├── index.tsx # Entry point
├── sw-main.ts # Service worker
└── ui/
├── workbench.tsx # Main UI component
├── actionList.tsx # Action timeline
├── callTab.tsx # Action details
├── snapshotTab.tsx # DOM snapshot viewer
├── networkTab.tsx # Network waterfall
├── consoleTab.tsx # Console messages
├── timeline.tsx # Time-based view
├── filmStrip.tsx # Video frames
├── logTab.tsx # Action logs
├── attachmentsTab.tsx # Files, screenshots
├── playbackControl.tsx # Video playback
└── [other tabs...]
```
### Data Flow
1. **Service Worker (`sw-main.ts`)** - Intercepts trace URL fetch
2. **Workbench Loader** - Loads trace via TraceLoader
3. **TraceModel** - Parses and indexes loaded data
4. **UI Components** - Display actions, snapshots, network, etc.
5. **Playback Control** - Synchronizes timeline with snapshots
### Key Data Models
- **TraceModel** - Loaded and parsed trace data
- **ActionTraceEventInContext** - Single action with context
- **Attachment** - File or screenshot data
- **SourceModel** - Source code + errors
---
## 12. CLI Commands
Located: `/home/pfeldman/code/playwright/packages/playwright-core/src/cli/program.ts`
### show-trace Command
```bash
playwright show-trace [trace] [options]
Options:
-b, --browser <browserType> Browser to use (chromium, firefox, webkit)
-h, --host <host> Host to serve on
-p, --port <port> Port to serve on (0 = any free port)
--stdin Accept trace URLs over stdin
Examples:
$ show-trace
$ show-trace https://example.com/trace.zip
$ show-trace /path/to/trace.zip
$ show-trace /path/to/trace/dir
```
### Implementation (program.ts: 327-355)
```typescript
program
.command('show-trace [trace]')
.option('-b, --browser <browserType>', ..., 'chromium')
.option('-h, --host <host>', 'Host to serve trace on')
.option('-p, --port <port>', 'Port to serve trace on')
.option('--stdin', 'Accept trace URLs over stdin')
.description('show trace viewer')
.action(function(trace, options) {
const openOptions: TraceViewerServerOptions = {
host: options.host,
port: +options.port,
isServer: !!options.stdin,
};
if (options.port !== undefined || options.host !== undefined)
runTraceInBrowser(trace, openOptions); // Opens in browser tab
else
runTraceViewerApp(trace, options.browser, openOptions); // Opens in app window
});
```
### Trace Viewer Server (traceViewer.ts)
```typescript
startTraceViewerServer(options?: TraceViewerServerOptions): Promise<HttpServer>
// Routes:
// GET /trace/file?path=<filePath> → Serve trace file
// GET /trace/file?path=<path>.json → Synthesize trace metadata
// GET /trace/file?path=<traceDir>/... → Serve trace.dir contents
// GET /trace/<other> → Serve viewer assets
runTraceViewerApp(traceUrl, browserName, options)
// Opens trace viewer in persistent browser context
runTraceInBrowser(traceUrl, options)
// Opens trace viewer in browser tab (tab.open)
```
---
## 13. Data Available Per Action
### Per-Action Data Structure
```typescript
ActionTraceEventInContext {
// Identifiers
callId: string; // Unique action ID
pageId?: string; // Associated page
parentId?: string; // Parent action (nested)
stepId?: string; // Test step ID
group?: ActionGroup; // Action category
// Timing
startTime: number; // Monotonic time (milliseconds)
endTime: number; // When action completed
// API Information
class: string; // Class name (Page, Frame, etc.)
method: string; // Method name (click, goto, etc.)
params: Record<string, any>; // Input parameters
result?: any; // Return value
// Code Location
stack?: StackFrame[]; // Call stack with file/line/column
title?: string; // User-facing name
// Snapshots
beforeSnapshot?: string; // "before@<callId>" reference
inputSnapshot?: string; // "input@<callId>" reference
afterSnapshot?: string; // "after@<callId>" reference
// Errors
error?: SerializedError; // Error message and stack
// Logging
log: { time: number, message: string }[]; // Action logs
// Attachments
attachments?: AfterActionTraceEventAttachment[];
// { name, contentType, path?, sha1?, base64? }
// Annotations
annotations?: AfterActionTraceEventAnnotation[];
// { type, description? }
// Interaction Details
point?: Point; // Pointer coordinates {x, y}
// Reference
context: ContextEntry; // Associated browser context
}
```
### Snapshot Data
Each snapshot can be accessed via `TraceLoader.storage()`:
```typescript
FrameSnapshot {
callId: string, // Associated action
pageId: string,
frameId: string,
frameUrl: string,
html: NodeSnapshot, // Encoded DOM tree
resourceOverrides: [ // Embedded resources
{ url, sha1?, ref? }
],
viewport: { width, height },
isMainFrame: boolean,
collectionTime: number, // ms to capture
timestamp: number, // Monotonic time
wallTime?: number,
}
```
### Network Data (HAR Entry)
```typescript
Entry {
request: {
method: string, // GET, POST, etc.
url: string,
httpVersion: string,
headers: Header[],
cookies: Cookie[],
queryString: { name, value }[],
postData?: {
mimeType: string,
params: Param[],
text: string,
_sha1?: string, // Reference to resources/
},
},
response: {
status: number, // 200, 404, etc.
statusText: string,
headers: Header[],
cookies: Cookie[],
content: {
size: number,
mimeType: string,
text?: string,
_sha1?: string, // Reference to resources/
compression?: number,
},
redirectURL: string,
},
timings: { // All in milliseconds
blocked?: number,
dns?: number,
connect?: number,
send: number,
wait: number, // Time to first byte
receive: number,
ssl?: number,
},
time: number, // Total time
_monotonicTime?: number, // Monotonic timestamp
_wasFulfilled?: boolean,
_wasAborted?: boolean,
_apiRequest?: boolean, // fetch/axios
}
```
---
## 14. Quick Reference: Accessing Trace Data
### In Trace Viewer
```typescript
// Load trace
const traceLoader = new TraceLoader();
const backend = new ZipTraceLoaderBackend('trace.zip');
await traceLoader.load(backend, (done, total) => {});
// Access context
const contextEntries = traceLoader.contextEntries;
// Get trace model
const traceModel = new TraceModel(traceUri, contextEntries);
// Iterate actions
for (const action of traceModel.actions) {
console.log(action.method); // e.g., "click"
console.log(action.params); // parameters
console.log(action.result); // return value
console.log(action.error); // error if failed
console.log(action.log); // log messages
}
// Get snapshots
const snapshotStorage = traceLoader.storage();
const snapshot = snapshotStorage.snapshotByName('before@<callId>');
// Get resource
const blob = await traceLoader.resourceForSha1(sha1);
```
### In Test Runner
```typescript
// Access via trace via browser context
const trace = await context.tracing.stop({ path: 'trace.zip' });
// Use server-side Tracing class
const tracing = new Tracing(context, tracesDir);
tracing.start({ snapshots: true, screenshots: true });
// ... run test ...
await tracing.stopChunk(progress, { mode: 'archive' });
await tracing.stop(progress);
```
---
## 15. Version Information
### Trace Format Versions
- **Version 3**: Early format (~1.35)
- **Version 4**: Updates (~1.36)
- **Version 5**: Improvements (~1.37)
- **Version 6**: Major changes (~10/2023, ~1.40)
- **Version 7**: Further updates (~05/2024, ~1.45)
- **Version 8**: Current format (latest)
### Compatibility
- Trace viewer automatically upgrades traces
- Newer viewer can read older traces
- Older viewer cannot read newer traces (TraceVersionError)
---
## 16. Key Design Patterns
### 1. Call ID Correlation
Every action uses a unique `callId` to correlate:
- Before event
- Input events
- Log messages
- After event
- Snapshots (before@callId, input@callId, after@callId)
- Attachments
- Network requests (indirect via timing)
### 2. Lazy Loading
- Snapshots stored by SHA1
- Resources fetched on demand
- JSONL format allows streaming
### 3. Snapshot References
- Instead of storing full DOM repeatedly
- Later snapshots reference earlier ones: `[[snapshotIndex, nodeIndex]]`
- Resources inlined via `resourceOverrides`
### 4. Dual Time Bases
- **wallTime**: Milliseconds since epoch (for display)
- **monotonicTime**: Internal monotonic clock (for correlation)
### 5. Chunked Recording
- Tests can have multiple chunks
- Each chunk has separate `.trace` file
- Network resources preserved across chunks
### 6. Grouping
- Actions can be grouped with `group()` / `groupEnd()`
- Used for test steps, fixtures
- Group tracking in `RecordingState.groupStack`
---
## 17. File Reference Guide
| File | Size | Purpose |
|------|------|---------|
| `trace/src/trace.ts` | 183 lines | Trace event types |
| `trace/src/har.ts` | 189 lines | Network HAR types |
| `trace/src/snapshot.ts` | 62 lines | Snapshot types |
| `playwright-core/.../tracing.ts` | 700+ lines | Recording engine |
| `playwright-core/.../traceParser.ts` | 62 lines | ZIP backend |
| `playwright-core/.../traceViewer.ts` | 288 lines | Viewer server |
| `playwright-core/.../traceLoader.ts` | 158 lines | Load traces |
| `playwright-core/.../traceModel.ts` | 300+ lines | Data model |
| `playwright-core/.../traceModernizer.ts` | 500+ lines | Version upgrades |
| `trace-viewer/src/index.tsx` | 48 lines | Viewer entry |
| `trace-viewer/src/ui/workbench.tsx` | Main UI | Display |

View File

@@ -0,0 +1,240 @@
# Vendor Dependencies & Bundling
Playwright ships a small number of node_modules inlined into a handful of
pre-built "bundle" files under `lib/`. Everything else is either a source
file compiled per-file, or loaded at runtime from one of the bundles. This
doc covers how the bundling works, how to add or move a vendored package,
and how the dependency checker enforces the contract.
## The Bundles
### playwright-core
| Output | Entry | Purpose |
|---|---|---|
| `lib/utilsBundle.js` | `src/utilsBundle.ts` | Vendored npm packages (`debug`, `mime`, `ws`, `yauzl`, `yazl`, `@modelcontextprotocol/sdk`, `graceful-fs`, …). The single home for third-party runtime code in playwright-core. |
| `lib/coreBundle.js` | `src/coreBundle.ts` | Re-exports of playwright-core's own modules (`client`, `iso`, `utils`, `cli`, `server`, `registry`, …) as namespaces. Inlines almost all playwright-core source except `utilsBundle`. |
| `lib/server/electron/loader.js` | `src/server/electron/loader.ts` | Tiny Electron preload shim. |
The `dynamicImportToRequirePlugin` in `utils/build/build.js` rewrites
vendored npm imports at **bundle time**. For example, a playwright-core
source file containing
```ts
import debug from 'debug';
```
gets rewritten to
```js
const debug = require('./utilsBundle').debug;
```
before the bundler sees it — so the vendored package never gets inlined
into `coreBundle.js`. The mapping from npm package name to utilsBundle
export key lives in `utils/build/utilsBundleMapping.js`.
### playwright
| Output | Entry | Purpose |
|---|---|---|
| `lib/transform/babelBundle.js` | `src/transform/babelBundle.ts` | Wraps `@babel/core`, `@babel/traverse`, `@babel/code-frame`, plugins. Shared by every consumer that needs babel. |
| `lib/transform/esmLoader.js` | `src/transform/esmLoader.ts` | Node ESM loader registered via `node:module.register()`. Output sits next to `babelBundle.js` so its `./babelBundle` sibling require resolves correctly. |
| `lib/common/index.js` | `src/common/index.ts` | Barrel of `common/*` + `transform/*` (compilationCache, test, configLoader, fixtures, globals, …). State-holding singletons (currentTestInfo, memoryCache, …) live here. |
| `lib/runner/index.js` | `src/runner/index.ts` | Barrel of `runner/*` + `reporters/*` + `plugins/*`. |
| `lib/matchers/expect.js` | `src/matchers/expect.ts` | Jest-style matchers with `expect` inlined. |
| `lib/worker/workerProcessEntry.js` | `src/worker/workerProcessEntry.ts` | Entry point spawned per test worker. |
| `lib/loader/loaderProcessEntry.js` | `src/loader/loaderProcessEntry.ts` | Entry point for the test file loader sub-process. |
| `lib/runner/uiModeReporter.js` | `src/runner/uiModeReporter.ts` | Loaded by `require.resolve` from testServer; passed to child workers as a file path. |
The `common` and `runner` bundles externalize `../transform/babelBundle`
(among other things) so babel code is not duplicated across them. The
`lib/transform/transform.ts` module uses `libPath('transform', 'babelBundle')`
(absolute path via `package.ts` root) to load the babel bundle at runtime,
so it works regardless of which bundle has inlined it.
### Per-file emits (no bundle)
Files outside the bundled entries are compiled 1:1 by esbuild and land
under `lib/` mirroring their source layout. The per-file step in
`utils/build/build.js` lists the specific directories for the
`playwright` package (`cli/`, `agents/`, `mcp/`, root `*.ts`, and a few
targeted files like `runner/uiModeReporter.ts`). Other packages
(`playwright-test`, `html-reporter`, `trace-viewer`, …) are compiled by
the generic per-package loop.
## Bundle Sidecars
Every bundled output has two sidecar files next to it:
- **`<bundle>.js.txt`** — human-readable report listing inlined files
(sorted by path, with per-file KB sizes), externals, and total bytes.
Written by `utils/build/bundle_report.js`.
- **`<bundle>.js.LICENSE`** — third-party license texts for every npm
package whose source got inlined. Populated from `license-checker`,
memoized once per build invocation. Consumed by the top-level
`ThirdPartyNotices.txt` files, which just point readers at the
per-bundle sidecars.
Both sidecars are included in the published npm package (controlled by
`packages/*/.npmignore`).
## Adding a Vendored NPM Dependency
Three pieces need to line up when adding a new npm package that you want
inlined into `utilsBundle` (i.e., loaded through `require('./utilsBundle').<key>`):
1. **Install the package.** Add it to the root `package.json`
`devDependencies`. The monorepo root is where esbuild resolves modules
from; the workspace root's `node_modules/<pkg>` is what gets inlined
into `utilsBundle.js`.
2. **Export it from `src/utilsBundle.ts`.** Pick one of:
```ts
import fooLibrary from 'foo';
export const foo = fooLibrary; // default
import * as fooLibrary from 'foo';
export const foo = fooLibrary; // namespace
export { namedSymbol } from 'foo'; // named
```
Type-only exports (`export type { X } from 'foo'`) are valid and
don't affect runtime.
3. **Add a mapping entry to `utils/build/utilsBundleMapping.js`**:
```js
'foo': { default: 'foo' },
// or:
'foo': { namespace: 'foo' },
// or:
'foo': { named: { namedSymbol: 'fooNamedSymbol' } },
```
- `default` — matches `import foo from 'foo'` and rewrites to
`require('./utilsBundle').foo`.
- `namespace` — matches `import * as foo from 'foo'`.
- `named` — matches `import { namedSymbol } from 'foo'` and rewrites
to `const { fooNamedSymbol: namedSymbol } = require('./utilsBundle')`.
- Multiple forms can coexist in one entry (see `yauzl`).
- The map key is the exact npm specifier as written in source
(including subpaths like `'@babel/core'` or `'colors/safe'`).
4. **Update DEPS.list.** The file or its enclosing folder's `DEPS.list`
must authorize `node_modules/<pkg>` — otherwise `npm run flint`'s
`check_deps` step complains about the disallowed external dependency.
If the DEPS.list authorizes it, the package.json-dependencies check
also gets skipped for that file.
5. **Run `npm run flint`.** It runs `check_deps`, `tsc`, `eslint`, and
`doc` in parallel. A missing mapping typically surfaces as `node_modules/`
references leaking into `coreBundle.js` — the build fails hard via
`assertCoreBundleHasNoNodeModules()`.
## In-tree Third-Party Helpers
Some vendored code isn't a published npm package but lives in-tree at
`packages/playwright-core/src/server/utils/third_party/` (e.g.
`extractZip.ts`, `lockfile.ts`). These are TypeScript files, not
node_modules. They're exposed to callers via two different routes:
- **Through `coreBundle.utils`.** Re-exported from
`src/server/utils/index.ts` via `export * from './third_party/extractZip'`
etc. Callers import via the `@utils/*` path alias:
```ts
import { extractZip } from '@utils/third_party/extractZip';
```
The alias is rewritten at bundle time to
`require('playwright-core/lib/coreBundle').utils.extractZip`.
- **Transitive npm deps via utilsBundle.** When a third_party TS file
imports an npm package (e.g., `lockfile.ts` imports `graceful-fs`,
`retry`, `signal-exit`), those are still rewritten through
`utilsBundle` — so the mapping in `utilsBundleMapping.js` must list
them too.
## DEPS.list
Every directory under `packages/*/src/` has a `DEPS.list` constraining
its imports. Three kinds of entries:
| Syntax | Meaning |
|---|---|
| `./somefile.ts`, `@isomorphic/**` | Relative or alias source import allowed |
| `node_modules/<pkg>` | npm package import allowed (exact specifier match) |
| `"strict"` | No other DEPS inherited; only what's listed is allowed |
Section headers `[filename.ts]` scope rules to a single file. The
top-level `[*]` (or no header) applies to everything in the folder plus
subfolders that don't have their own DEPS.list.
A DEPS.list entry of `node_modules/<pkg>` now shortcuts both layers of
the check: the "disallowed external dependency" error AND the
"dependencies not declared in package.json" report. The per-file
allowlist is the contract — no need to also list the dep in
`packages/<pkg>/package.json` if only one file uses it and it's
authorized there.
### check_deps.js
`utils/check_deps.js` walks the TypeScript program, visits every
`import` in `src/**`, and for each npm specifier:
1. Skips if the source file's DEPS.list authorizes `node_modules/<specifier>`.
2. Otherwise records the top-level package name along with the file
path that imported it.
3. Subtracts `peerDependencies`, `VENDORED_PACKAGES` (from
`utilsBundleMapping.js`), and any package that resolves without
`node_modules/` (a core module or a local file).
4. Subtracts packages listed in `packages/<pkg>/package.json`
`dependencies`.
5. Anything left is reported with the specific file(s) that import it.
The missing-dep error now includes file paths:
```
Dependencies are not declared in package.json:
expect
src/matchers/expect.ts
@babel/core
src/transform/babelBundle.ts
```
## Bundle-Level Externalization (onResolve plugins)
Two onResolve plugins in `utils/build/build.js` normalize relative
imports to the sibling bundle at consumer output level:
- **`externalizeUtilsBundlePlugin`** — matches any relative specifier
ending in `/utilsBundle` or `/utilsBundle.js` (at any depth: `./utilsBundle`,
`../utilsBundle`, `../../utilsBundle`) and marks it external with the
single spelling `./utilsBundle`. This only applies to the coreBundle
build because coreBundle inlines source files from all over
`playwright-core/src/` (different depths) and needs a single consistent
external specifier that resolves correctly at runtime from
`lib/coreBundle.js`.
- The **babelBundle** case is handled differently — instead of a plugin,
consumers' source/output depths are aligned:
- `esmLoader` bundle output is placed at `lib/transform/esmLoader.js`
(same folder as `babelBundle.js`), so `./babelBundle` from
`transform.ts` resolves correctly.
- `common` and `runner` bundles declare `'../transform/babelBundle'`
as a static external; their outputs are at `lib/common/index.js` and
`lib/runner/index.js`, both at depth 1, so the source-relative
specifier resolves naturally.
- `transform.ts`'s own `require('./babelBundle')` was replaced with
`require(libPath('transform', 'babelBundle'))` — an absolute path
computed at runtime via `package.ts`, which works from any bundle.
## Quick Reference
- To add a new vendored npm dep: root `package.json` → `utilsBundle.ts`
export → `utilsBundleMapping.js` entry → DEPS.list → `npm run flint`.
- To add a new in-tree third-party helper: drop the `.ts` file under
`server/utils/third_party/`, re-export from `server/utils/index.ts`,
and use `@utils/third_party/<name>` at call sites.
- To add a new bundle entry: add an `EsbuildStep` in
`utils/build/build.js`, pick output location so relative externals
line up with runtime layout, and list externals for every sibling
bundle the entry should not inline.
- To expose a bundle file as a package subpath: add it to the
`exports` field in `packages/<pkg>/package.json`.
- To check what's inside a bundle: read the `.js.txt` sidecar next to
the output.

View File

@@ -0,0 +1,22 @@
# Updating WebKit Safari Version
The Safari version string used in the WebKit user-agent is declared in one place:
**[packages/playwright-core/src/server/webkit/wkBrowser.ts](../../../packages/playwright-core/src/server/webkit/wkBrowser.ts)** — `BROWSER_VERSION` constant (line ~35).
```ts
const BROWSER_VERSION = '26.4';
const DEFAULT_USER_AGENT = `Mozilla/5.0 ... Version/${BROWSER_VERSION} Safari/605.1.15`;
```
## Steps to update
1. **Find the latest stable Safari version** — search `site:developer.apple.com "Safari X.Y Release Notes"` or check the [Safari Release Notes](https://developer.apple.com/documentation/safari-release-notes) index. The highest numbered entry that is not a Technology Preview is the current stable release.
2. **Update `BROWSER_VERSION`** in `wkBrowser.ts`.
3. **Run lint** to update any generated files that embed the version:
```bash
npm run flint
```

View File

@@ -0,0 +1,12 @@
---
name: playwright-devops
description: DevOps workflows for Playwright - CI failure analysis, workflow debugging, and release operations.
user_invocable: true
---
# Playwright DevOps
## Guides
- [CI Commit Failure Report](commit-failures.md) — analyze GitHub Actions failures for the last commit on main
- [fetch-commit-logs.sh](fetch-commit-logs.sh) — script to download failed job logs into `~/tmp/commit-<sha>/`

View File

@@ -0,0 +1,69 @@
# CI Health Report
Generate a CI health report for the last commit on the `main` branch of `microsoft/playwright`.
This is an overall tree health report — not a commit regression analysis. The goal is to show
the full picture of what's failing, grouping by root cause. If any failures appear to be
regressions introduced by this specific commit, call that out, but most failures will be
pre-existing flakes, infrastructure issues, or platform-specific problems.
## Phase 1 — Fetch logs
Run the fetch script to download all failed job logs into a local folder:
```
bash .claude/skills/playwright-devops/fetch-commit-logs.sh [<sha>]
```
- If no SHA is provided, it fetches the last commit on `main`.
- Creates `~/tmp/commit-<short-sha>/` with:
- `summary.json` — commit info, failed workflows, and failed job metadata
- `<workflow-name>/<job-name>.log` — failed log output for each failed job
**Note:** The script fetches failed jobs from both failed AND in-progress workflows.
Workflows may still be running while some of their jobs have already failed — these
must be included in the report. If any workflows are still in progress, note this in
the report summary.
## Phase 2 — Analyze
1. **Read `summary.json`** to get the commit message and the list of failed workflows/jobs.
2. **Read each `.log` file** and extract failing test names and error messages.
3. **Compile the report leading with a summary**, then detailed tables:
```markdown
# CI Health Report — <short-sha>
Commit: `<commit message>`
## Summary
Brief overview: N workflows, N failed jobs, N total test failures.
Note if any workflows are still in progress.
### Possible regressions (may be related to this commit)
- **N failures** in `test/file.spec.ts` across <browsers/platforms> — <brief description>
### Pre-existing / flaky
- **N failures** in `test/file.spec.ts` — <brief description> (timeouts, infrastructure, platform-specific)
### Infrastructure issues
- <description of non-test failures>
---
## Detailed Failures
### Workflow: <name> (run <id>)
#### <job name> (job <id>) -- N failures
| Test | Error |
|------|-------|
| `path/to/test.spec.ts:line` -- test title | error message |
```
The summary should appear first so readers immediately see what matters. Group related failures
(e.g. same test failing across browsers) into single summary bullet points rather than listing each individually.
4. **Save the report** to `ci-failures-<short-sha>.md` in the repo root.

View File

@@ -0,0 +1,107 @@
#!/bin/bash
set -euo pipefail
# Usage: fetch-commit-logs.sh [<sha>]
# Fetches CI failure logs for a commit into ~/tmp/commit-<short-sha>/
REPO="microsoft/playwright"
REF="${1:-main}"
# Resolve commit
COMMIT_JSON=$(gh api "repos/$REPO/commits/$REF" --jq '{sha: .sha, message: .commit.message}')
SHA=$(echo "$COMMIT_JSON" | jq -r '.sha')
SHORT_SHA="${SHA:0:7}"
MESSAGE=$(echo "$COMMIT_JSON" | jq -r '.message' | head -1)
OUTDIR="$HOME/tmp/commit-$SHORT_SHA"
mkdir -p "$OUTDIR"
echo "Commit: $SHORT_SHA$MESSAGE"
echo "Output: $OUTDIR"
# Get all workflow runs for this commit
RUNS_JSON=$(gh api "repos/$REPO/actions/runs?head_sha=$SHA&per_page=50" \
--jq '[.workflow_runs[] | {id, name, conclusion, workflow_id}]')
# Filter to failed workflows
FAILED_RUNS=$(echo "$RUNS_JSON" | jq -c '[.[] | select(.conclusion == "failure" or .conclusion == null)]')
FAILED_COUNT=$(echo "$FAILED_RUNS" | jq 'length')
if [ "$FAILED_COUNT" -eq 0 ]; then
echo "No failed workflows."
echo '{"sha":"'"$SHA"'","short_sha":"'"$SHORT_SHA"'","message":"'"$MESSAGE"'","workflows":[]}' | jq . > "$OUTDIR/summary.json"
echo "$OUTDIR"
exit 0
fi
echo "Found $FAILED_COUNT failed workflow(s). Fetching failed jobs..."
# Build summary and collect jobs to fetch
SUMMARY='{"sha":"'"$SHA"'","short_sha":"'"$SHORT_SHA"'","message":'"$(echo "$MESSAGE" | jq -Rs .)"',"workflows":[]}'
sanitize() {
echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._-]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//'
}
PIDS=()
for i in $(seq 0 $((FAILED_COUNT - 1))); do
RUN=$(echo "$FAILED_RUNS" | jq -c ".[$i]")
RUN_ID=$(echo "$RUN" | jq -r '.id')
RUN_NAME=$(echo "$RUN" | jq -r '.name')
WORKFLOW_DIR=$(sanitize "$RUN_NAME")
# Get failed jobs for this run
JOBS_JSON=$(gh run view "$RUN_ID" --json jobs \
--jq '[.jobs[] | select(.conclusion == "failure") | {name: .name, id: .databaseId}]')
JOB_COUNT=$(echo "$JOBS_JSON" | jq 'length')
if [ "$JOB_COUNT" -eq 0 ]; then
continue
fi
mkdir -p "$OUTDIR/$WORKFLOW_DIR"
# Add to summary
WORKFLOW_ENTRY=$(jq -n \
--arg name "$RUN_NAME" \
--argjson id "$RUN_ID" \
--argjson jobs "$JOBS_JSON" \
'{name: $name, id: $id, failed_jobs: $jobs}')
SUMMARY=$(echo "$SUMMARY" | jq --argjson w "$WORKFLOW_ENTRY" '.workflows += [$w]')
# Fetch logs in parallel
for j in $(seq 0 $((JOB_COUNT - 1))); do
JOB=$(echo "$JOBS_JSON" | jq -c ".[$j]")
JOB_ID=$(echo "$JOB" | jq -r '.id')
JOB_NAME=$(echo "$JOB" | jq -r '.name')
JOB_FILE=$(sanitize "$JOB_NAME")
LOG_PATH="$OUTDIR/$WORKFLOW_DIR/$JOB_FILE.log"
(
echo "# $JOB_NAME" > "$LOG_PATH"
echo "" >> "$LOG_PATH"
gh run view --job "$JOB_ID" --log-failed 2>&1 | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' | sed $'s/^[^\t]*\t[^\t]*\t\xef\xbb\xbf\{0,1\}[0-9T:.Z-]* //' >> "$LOG_PATH" || true
# If only header (e.g. workflow still in progress), fetch via jobs API
if [ "$(wc -l < "$LOG_PATH")" -le 3 ]; then
echo "# $JOB_NAME" > "$LOG_PATH"
echo "" >> "$LOG_PATH"
gh api "repos/$REPO/actions/jobs/$JOB_ID/logs" 2>&1 | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' | sed $'s/\xef\xbb\xbf//g' | sed 's/^[0-9T:.Z-]* //' >> "$LOG_PATH" || true
fi
echo " Fetched: $WORKFLOW_DIR/$JOB_FILE.log"
) &
PIDS+=($!)
done
done
# Wait for all parallel fetches
for pid in "${PIDS[@]}"; do
wait "$pid" 2>/dev/null || true
done
# Write summary
echo "$SUMMARY" | jq . > "$OUTDIR/summary.json"
echo ""
echo "Done. Logs saved to: $OUTDIR"
echo "$OUTDIR"