참고소스 수정본
This commit is contained in:
18
참고/playwright-main/.claude/skills/playwright-dev/SKILL.md
Normal file
18
참고/playwright-main/.claude/skills/playwright-dev/SKILL.md
Normal 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
|
||||
295
참고/playwright-main/.claude/skills/playwright-dev/api.md
Normal file
295
참고/playwright-main/.claude/skills/playwright-dev/api.md
Normal 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)
|
||||
```
|
||||
@@ -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 1–3 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.
|
||||
@@ -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`.
|
||||
418
참고/playwright-main/.claude/skills/playwright-dev/library.md
Normal file
418
참고/playwright-main/.claude/skills/playwright-dev/library.md
Normal 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`.
|
||||
512
참고/playwright-main/.claude/skills/playwright-dev/tools.md
Normal file
512
참고/playwright-main/.claude/skills/playwright-dev/tools.md
Normal 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
|
||||
```
|
||||
@@ -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 |
|
||||
|
||||
240
참고/playwright-main/.claude/skills/playwright-dev/vendor.md
Normal file
240
참고/playwright-main/.claude/skills/playwright-dev/vendor.md
Normal 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.
|
||||
@@ -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
|
||||
```
|
||||
12
참고/playwright-main/.claude/skills/playwright-devops/SKILL.md
Normal file
12
참고/playwright-main/.claude/skills/playwright-devops/SKILL.md
Normal 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>/`
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user