# 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 |

### 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`.
