|
| 1 | +# Adding and Modifying APIs |
| 2 | + |
| 3 | +- 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. |
| 4 | + |
| 5 | +## Step 1: Define API in Documentation |
| 6 | + |
| 7 | +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`). |
| 8 | + |
| 9 | +### Documentation Format |
| 10 | + |
| 11 | +**Method definition:** |
| 12 | +```markdown |
| 13 | +## async method: Page.methodName |
| 14 | +* since: v1.XX |
| 15 | +- returns: <[null]|[Response]> |
| 16 | + |
| 17 | +Description of the method. |
| 18 | + |
| 19 | +### param: Page.methodName.paramName |
| 20 | +* since: v1.XX |
| 21 | +- `paramName` <[string]> |
| 22 | + |
| 23 | +Description of the parameter. |
| 24 | + |
| 25 | +### option: Page.methodName.optionName |
| 26 | +* since: v1.XX |
| 27 | +- `optionName` <[string]> |
| 28 | + |
| 29 | +Description of the option. |
| 30 | +``` |
| 31 | + |
| 32 | +**Key syntax rules:** |
| 33 | +- `* since: v1.XX` — version from package.json (without -next) |
| 34 | +- `* langs: js, python` — language filter (optional) |
| 35 | +- `* langs: alias-java: navigate` — language-specific method name |
| 36 | +- `* deprecated: v1.XX` — deprecation marker |
| 37 | +- `<[TypeName]>` — type annotation: `<[string]>`, `<[int]>`, `<[float]>`, `<[boolean]>` |
| 38 | +- `<[null]|[Response]>` — union type |
| 39 | +- `<[Array]<[Locator]>>` — array type |
| 40 | +- `<[Object]>` with indented `- \`field\` <[type]>` — object type |
| 41 | +- `### param:` — required parameter |
| 42 | +- `### option:` — optional parameter |
| 43 | +- `= %%-placeholder-name-%%` — reuse shared param definition from `docs/src/api/params.md` |
| 44 | + |
| 45 | +**Property definition:** |
| 46 | +```markdown |
| 47 | +## property: Page.propName |
| 48 | +* since: v1.XX |
| 49 | +- type: <[string]> |
| 50 | + |
| 51 | +Description. |
| 52 | +``` |
| 53 | + |
| 54 | +**Event definition:** |
| 55 | +```markdown |
| 56 | +## event: Page.eventName |
| 57 | +* since: v1.XX |
| 58 | +- argument: <[Dialog]> |
| 59 | + |
| 60 | +Description. |
| 61 | +``` |
| 62 | + |
| 63 | +Watch will kick in and auto-generate: |
| 64 | +- `packages/playwright-core/types/types.d.ts` — public API types |
| 65 | +- `packages/playwright/types/test.d.ts` — test API types |
| 66 | + |
| 67 | +## Step 2: Implement Client API |
| 68 | + |
| 69 | +Implement the new API in `packages/playwright-core/src/client/xxx.ts`. |
| 70 | + |
| 71 | +### Client Implementation Pattern |
| 72 | + |
| 73 | +Client classes extend `ChannelOwner<XxxChannel>` and call through `this._channel`: |
| 74 | + |
| 75 | +```typescript |
| 76 | +// Direct channel call (most common) |
| 77 | +async methodName(param: string, options: channels.FrameMethodNameOptions = {}): Promise<void> { |
| 78 | + await this._channel.methodName({ param, ...options, timeout: this._timeout(options) }); |
| 79 | +} |
| 80 | + |
| 81 | +// Channel call with response wrapping |
| 82 | +async goto(url: string, options: channels.FrameGotoOptions = {}): Promise<network.Response | null> { |
| 83 | + return network.Response.fromNullable( |
| 84 | + (await this._channel.goto({ url, ...options, timeout: this._timeout(options) })).response |
| 85 | + ); |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +**Key patterns:** |
| 90 | +- Parameters are assembled into a single object for the channel call |
| 91 | +- Timeout is processed through `this._timeout(options)` or `this._navigationTimeout(options)` |
| 92 | +- Return values from channel are unwrapped/converted: `Response.fromNullable()`, `ElementHandle.from()`, etc. |
| 93 | +- Locator methods delegate to Frame: `return await this._frame.click(this._selector, { strict: true, ...options })` |
| 94 | +- Page methods often delegate to `this._mainFrame` |
| 95 | + |
| 96 | +## Step 3: Define Protocol Channel |
| 97 | + |
| 98 | +Define (or update) channel for the API in `packages/protocol/src/protocol.yml` as needed. |
| 99 | + |
| 100 | +### Protocol YAML Format |
| 101 | + |
| 102 | +Methods are defined under `commands:` in the interface section: |
| 103 | + |
| 104 | +```yaml |
| 105 | +Page: |
| 106 | + type: interface |
| 107 | + extends: EventTarget |
| 108 | + |
| 109 | + commands: |
| 110 | + methodName: |
| 111 | + title: Short description for tracing |
| 112 | + parameters: |
| 113 | + url: string # required string |
| 114 | + timeout: float # required float |
| 115 | + referer: string? # optional string (? suffix) |
| 116 | + waitUntil: LifecycleEvent? # optional reference to another type |
| 117 | + button: # optional enum |
| 118 | + type: enum? |
| 119 | + literals: |
| 120 | + - left |
| 121 | + - right |
| 122 | + - middle |
| 123 | + modifiers: # optional array of enums |
| 124 | + type: array? |
| 125 | + items: |
| 126 | + type: enum |
| 127 | + literals: |
| 128 | + - Alt |
| 129 | + - Control |
| 130 | + - Meta |
| 131 | + - Shift |
| 132 | + position: Point? # optional reference type |
| 133 | + viewportSize: # required inline object |
| 134 | + type: object |
| 135 | + properties: |
| 136 | + width: int |
| 137 | + height: int |
| 138 | + returns: |
| 139 | + response: Response? # optional return value |
| 140 | + flags: |
| 141 | + slowMo: true |
| 142 | + snapshot: true |
| 143 | + pausesBeforeAction: true |
| 144 | +``` |
| 145 | +
|
| 146 | +**Type primitives:** `string`, `int`, `float`, `boolean`, `binary`, `json` |
| 147 | +**Optional:** append `?` to any type: `string?`, `int?`, `object?` |
| 148 | +**Arrays:** `type: array` with `items:` (or `type: array?` for optional) |
| 149 | +**Enums:** `type: enum` with `literals:` list |
| 150 | +**References:** use type name directly: `Response`, `Frame`, `Point` |
| 151 | +**Flags:** `slowMo`, `snapshot`, `pausesBeforeAction`, `pausesBeforeInput` |
| 152 | + |
| 153 | +Watch will kick in and auto-generate: |
| 154 | +- `packages/protocol/src/channels.d.ts` — channel TypeScript interfaces |
| 155 | +- `packages/playwright-core/src/protocol/validator.ts` — runtime validators |
| 156 | +- `packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts` — method metadata |
| 157 | + |
| 158 | +## Step 4: Implement Dispatcher |
| 159 | + |
| 160 | +Implement dispatcher handler in `packages/playwright-core/src/server/dispatchers/xxxDispatcher.ts` as needed. |
| 161 | + |
| 162 | +### Dispatcher Pattern |
| 163 | + |
| 164 | +Dispatchers receive validated params and route to server objects: |
| 165 | + |
| 166 | +```typescript |
| 167 | +// Simple pass-through (most common) |
| 168 | +async methodName(params: channels.PageMethodNameParams, progress: Progress): Promise<void> { |
| 169 | + await this._page.methodName(progress, params.value); |
| 170 | +} |
| 171 | +
|
| 172 | +// With response wrapping |
| 173 | +async goto(params: channels.FrameGotoParams, progress: Progress): Promise<channels.FrameGotoResult> { |
| 174 | + return { response: ResponseDispatcher.fromNullable(this._browserContextDispatcher, |
| 175 | + await this._frame.goto(progress, params.url, params)) }; |
| 176 | +} |
| 177 | +
|
| 178 | +// With dispatcher extraction (when params contain dispatcher references) |
| 179 | +async expectScreenshot(params: channels.PageExpectScreenshotParams, progress: Progress): Promise<channels.PageExpectScreenshotResult> { |
| 180 | + const mask = (params.mask || []).map(({ frame, selector }) => ({ |
| 181 | + frame: (frame as FrameDispatcher)._object, |
| 182 | + selector, |
| 183 | + })); |
| 184 | + return await this._page.expectScreenshot(progress, { ...params, mask }); |
| 185 | +} |
| 186 | +
|
| 187 | +// With array result wrapping |
| 188 | +async querySelectorAll(params: channels.FrameQuerySelectorAllParams, progress: Progress): Promise<channels.FrameQuerySelectorAllResult> { |
| 189 | + const elements = await progress.race(this._frame.querySelectorAll(params.selector)); |
| 190 | + return { elements: elements.map(e => ElementHandleDispatcher.from(this, e)) }; |
| 191 | +} |
| 192 | +``` |
| 193 | + |
| 194 | +**Key patterns:** |
| 195 | +- Method signature: `async method(params: channels.XxxMethodParams, progress: Progress): Promise<channels.XxxMethodResult>` |
| 196 | +- Extract params: `params.url`, `params.selector`, etc. |
| 197 | +- Convert dispatcher refs to server objects: `(params.frame as FrameDispatcher)._object` |
| 198 | +- Wrap server objects as dispatchers in results: `ResponseDispatcher.fromNullable()`, `ElementHandleDispatcher.from()` |
| 199 | +- All methods receive `Progress` for timeout/cancellation |
| 200 | + |
| 201 | +## Step 5: Implement Server Logic |
| 202 | + |
| 203 | +Handler should route the call into the corresponding method in `packages/playwright-core/src/server/xxx.ts`. |
| 204 | + |
| 205 | +Server methods implement the actual browser interaction: |
| 206 | + |
| 207 | +```typescript |
| 208 | +// In packages/playwright-core/src/server/frames.ts |
| 209 | +async goto(progress: Progress, url: string, options: types.GotoOptions = {}): Promise<network.Response | null> { |
| 210 | + // ... validation, URL construction ... |
| 211 | + // Delegates to browser-specific implementation: |
| 212 | + const result = await this._page.delegate.navigateFrame(this, url, referer); |
| 213 | + // ... wait for lifecycle events ... |
| 214 | + return response; |
| 215 | +} |
| 216 | +``` |
| 217 | + |
| 218 | +Browser-specific implementations live in: |
| 219 | +- `packages/playwright-core/src/server/chromium/crPage.ts` — Chromium (uses CDP: `this._client.send('Page.navigate', { ... })`) |
| 220 | +- `packages/playwright-core/src/server/firefox/ffPage.ts` — Firefox |
| 221 | +- `packages/playwright-core/src/server/webkit/wkPage.ts` — WebKit |
| 222 | + |
| 223 | +## Step 6: Write Tests |
| 224 | + |
| 225 | +### Test Location |
| 226 | +- Page-only tests: `tests/page/xxx.spec.ts` — use `page` fixture |
| 227 | +- Context tests: `tests/library/xxx.spec.ts` — use `context` fixture |
| 228 | + |
| 229 | +### Test Patterns |
| 230 | + |
| 231 | +**Page test:** |
| 232 | +```typescript |
| 233 | +import { test as it, expect } from './pageTest'; |
| 234 | +
|
| 235 | +it('should do something @smoke', async ({ page, server }) => { |
| 236 | + await page.goto(server.EMPTY_PAGE); |
| 237 | + // ... assertions ... |
| 238 | + expect(page.url()).toBe(server.EMPTY_PAGE); |
| 239 | +}); |
| 240 | +
|
| 241 | +it('should handle options', async ({ page, server, browserName, isAndroid }) => { |
| 242 | + it.skip(isAndroid, 'Not supported on Android'); |
| 243 | + it.info().annotations.push({ type: 'issue', description: 'https://github.com/user/repo/issues/123' }); |
| 244 | + // ... |
| 245 | +}); |
| 246 | +``` |
| 247 | + |
| 248 | +**Library/context test:** |
| 249 | +```typescript |
| 250 | +import { contextTest as it, expect } from '../config/browserTest'; |
| 251 | +
|
| 252 | +it('should work with context', async ({ context, server }) => { |
| 253 | + const page = await context.newPage(); |
| 254 | + await page.goto(server.EMPTY_PAGE); |
| 255 | + // ... |
| 256 | +}); |
| 257 | +``` |
| 258 | + |
| 259 | +### Available Fixtures |
| 260 | +- `page` — isolated page instance |
| 261 | +- `context` — browser context (library tests) |
| 262 | +- `server` — HTTP test server (`server.EMPTY_PAGE`, `server.PREFIX`, `server.CROSS_PROCESS_PREFIX`) |
| 263 | +- `httpsServer` — HTTPS test server |
| 264 | +- `asset(name)` — path to test asset file |
| 265 | +- `browserName` — `'chromium' | 'firefox' | 'webkit'` |
| 266 | +- `channel` — browser channel string |
| 267 | +- `isAndroid`, `isBidi`, `isElectron` — platform booleans |
| 268 | +- `isWindows`, `isMac`, `isLinux` — OS booleans |
| 269 | +- `mode` — test mode (`'default'`, `'service'`, etc.) |
| 270 | + |
| 271 | +### Running Tests |
| 272 | +```bash |
| 273 | +npm run ctest tests/page/xxx.spec.ts # Chromium only |
| 274 | +npm run test tests/page/xxx.spec.ts # All browsers |
| 275 | +npm run ctest -- --grep "should do something" # Filter by name |
| 276 | +``` |
| 277 | + |
| 278 | +## Architecture Overview |
| 279 | + |
| 280 | +``` |
| 281 | +docs/src/api/class-xxx.md (API documentation — source of truth for public types) |
| 282 | + → auto-generates → types.d.ts, test.d.ts |
| 283 | + |
| 284 | +packages/protocol/src/protocol.yml (RPC protocol definition) |
| 285 | + → auto-generates → channels.d.ts, validator.ts, protocolMetainfo.ts |
| 286 | + |
| 287 | +Client call chain: |
| 288 | + user code → Page.method() → Frame.method() → this._channel.method(params) |
| 289 | + → Proxy validates & sends → Connection.sendMessageToServer() |
| 290 | + → [wire] → |
| 291 | + DispatcherConnection.dispatch() → XxxDispatcher.method(params, progress) |
| 292 | + → ServerObject.method(progress, ...) → BrowserDelegate (CDP/Firefox/WebKit) |
| 293 | +``` |
0 commit comments