skills/playwright-dev/library.md

419 lines
17 KiB
Markdown

# 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`.
**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.
## 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 |
### Public API Exports
`packages/playwright-core/src/client/api.ts` exports all public classes.
## 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`.