Skip to content

Latest commit

 

History

History
422 lines (327 loc) · 17.7 KB

File metadata and controls

422 lines (327 loc) · 17.7 KB

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.

Protocol Layer

protocol.yml

Defines all RPC interfaces, commands (methods), events, and types. Example:

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:

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.

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:

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.

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.

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:

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.