Skip to content

Commit 4246deb

Browse files
committed
chore(api): context.rc
1 parent bfd1ec6 commit 4246deb

File tree

31 files changed

+1505
-7
lines changed

31 files changed

+1505
-7
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
name: playwright-api
3+
description: Explains how to add playwright API methods.
4+
---
5+
6+
# API
7+
8+
## Adding and modifying APIs
9+
- 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.
10+
- Define (or update) API in `docs/api/class-xxx.md`. For the new methods, params and options use the version from package.json (without -next).
11+
- Watch will kick in and re-generate types for the API
12+
- Implement the new API in `packages/playwright/src/client/xxx.ts`
13+
- Define (or update) channel for the API in `packages/protocol/src/protocol.yml` as needed
14+
- Watch will kick in and re-generate types for protocol channels
15+
- Implement dispatcher handler in `packages/playwright/src/server/dispatchers/xxxDispatcher.ts` as needed
16+
- Handler should just route the call into the corresponding method in `packages/playwright-core/src/server/xxx.ts`
17+
- Place new tests in `tests/page/xxx.spec.ts` or create new test file if needed
18+
19+
# Build
20+
- Assume watch is running and everything is up to date.
21+
22+
# Test
23+
- If your tests are only using page, prefer to place them in `tests/page/xxx.spec.ts` and use page fixture. If you need to use browser context, place them in `tests/library/xxx.spec.ts`.
24+
- Run npm test as `npm run ctest <file>`
25+
26+
# Lint
27+
- In the end lint via `npm run flint`.

.claude/skills/playwright-mcp-dev/SKILL.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ description: Explains how to add and debug playwright MCP tools and CLI commands
3030
in `packages/playwright/src/skill/references/`
3131
- Place new tests in `tests/mcp/cli-<category>.spec.ts`
3232

33+
## Adding CLI options or Config options
34+
When you need to add something to config.
35+
36+
- `packages/playwright/src/mcp/program.ts`
37+
- add CLI option and doc
38+
- `packages/playwright/src/mcp/config.d.ts`
39+
- add and document the option
40+
- `packages/playwright/src/mcp/config.ts`
41+
- modify FullConfig if needed
42+
- and CLIOptions if needed
43+
- add it to configFromEnv
44+
3345
## Building
3446
- Assume watch is running at all times, run lint to see type errors
3547

docs/src/api/class-browsercontext.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,12 @@ Allows to wait for async listeners to complete or to ignore subsequent errors fr
10071007
### option: BrowserContext.removeAllListeners.behavior = %%-remove-all-listeners-options-behavior-%%
10081008
* since: v1.47
10091009

1010+
## property: BrowserContext.rc
1011+
* since: v1.59
1012+
- type: <[RC]>
1013+
1014+
Remote control for the browser context. Can be used to stream live screencast of all pages in the context with a tabbed UI.
1015+
10101016
## property: BrowserContext.request
10111017
* since: v1.16
10121018
* langs:

docs/src/api/class-rc.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# class: RC
2+
* since: v1.59
3+
4+
Remote control allows streaming live screencast of the browser context over WebSocket.
5+
It shows a tabbed browser-like UI with all context pages.
6+
7+
## async method: RC.startHttp
8+
* since: v1.59
9+
- returns: <[Object]>
10+
- `url` <[string]> URL of the screencast server.
11+
12+
Starts an HTTP server that streams live screencast frames over WebSocket. Returns an object with the server URL.
13+
Open the URL in a browser to see the live screencast with a tabbed UI showing all pages in the context.
14+
15+
**Usage**
16+
17+
```js
18+
const { url } = await context.rc.startHttp();
19+
console.log('Open to view screencast:', url);
20+
// ... perform actions ...
21+
await context.rc.stopHttp();
22+
```
23+
24+
### option: RC.startHttp.size
25+
* since: v1.59
26+
- `size` ?<[Object]>
27+
- `width` <[int]> Video frame width.
28+
- `height` <[int]> Video frame height.
29+
30+
Optional dimensions of the screencast frames. If not specified the size will be scaled down to fit into 800x800.
31+
32+
### option: RC.startHttp.port
33+
* since: v1.59
34+
- `port` ?<[int]>
35+
36+
Port to bind the HTTP server to. If not specified, a random available port will be used.
37+
38+
### option: RC.startHttp.host
39+
* since: v1.59
40+
- `host` ?<[string]>
41+
42+
Host to bind the HTTP server to. Default is localhost.
43+
44+
## async method: RC.stopHttp
45+
* since: v1.59
46+
47+
Stops the screencast server started with [`method: RC.startHttp`].

packages/playwright-client/types/types.d.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9911,6 +9911,12 @@ export interface BrowserContext {
99119911
*/
99129912
clock: Clock;
99139913

9914+
/**
9915+
* Remote control for the browser context. Can be used to stream live screencast of all pages in the context with a
9916+
* tabbed UI.
9917+
*/
9918+
rc: RC;
9919+
99149920
/**
99159921
* API testing helper associated with this context. Requests made with this API will use context cookies.
99169922
*/
@@ -20748,6 +20754,65 @@ export const selectors: Selectors;
2074820754
*/
2074920755
export const webkit: BrowserType;
2075020756

20757+
/**
20758+
* Remote control allows streaming live screencast of the browser context over WebSocket. It shows a tabbed
20759+
* browser-like UI with all context pages.
20760+
*/
20761+
export interface RC {
20762+
/**
20763+
* Starts an HTTP server that streams live screencast frames over WebSocket. Returns an object with the server URL.
20764+
* Open the URL in a browser to see the live screencast with a tabbed UI showing all pages in the context.
20765+
*
20766+
* **Usage**
20767+
*
20768+
* ```js
20769+
* const { url } = await context.rc.startHttp();
20770+
* console.log('Open to view screencast:', url);
20771+
* // ... perform actions ...
20772+
* await context.rc.stopHttp();
20773+
* ```
20774+
*
20775+
* @param options
20776+
*/
20777+
startHttp(options?: {
20778+
/**
20779+
* Host to bind the HTTP server to. Default is localhost.
20780+
*/
20781+
host?: string;
20782+
20783+
/**
20784+
* Port to bind the HTTP server to. If not specified, a random available port will be used.
20785+
*/
20786+
port?: number;
20787+
20788+
/**
20789+
* Optional dimensions of the screencast frames. If not specified the size will be scaled down to fit into 800x800.
20790+
*/
20791+
size?: {
20792+
/**
20793+
* Video frame width.
20794+
*/
20795+
width: number;
20796+
20797+
/**
20798+
* Video frame height.
20799+
*/
20800+
height: number;
20801+
};
20802+
}): Promise<{
20803+
/**
20804+
* URL of the screencast server.
20805+
*/
20806+
url: string;
20807+
}>;
20808+
20809+
/**
20810+
* Stops the screencast server started with
20811+
* [rC.startHttp([options])](https://playwright.dev/docs/api/class-rc#rc-start-http).
20812+
*/
20813+
stopHttp(): Promise<void>;
20814+
}
20815+
2075120816
/**
2075220817
* Whenever the page sends a request for a network resource the following sequence of events are emitted by
2075320818
* [Page](https://playwright.dev/docs/api/class-page):

packages/playwright-core/src/client/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export { Request, Response, Route, WebSocket, WebSocketRoute } from './network';
3737
export { APIRequest, APIRequestContext, APIResponse } from './fetch';
3838
export { Page } from './page';
3939
export { PageAgent } from './pageAgent';
40+
export { RC } from './rc';
4041
export { Selectors } from './selectors';
4142
export { Tracing } from './tracing';
4243
export { Video } from './video';

packages/playwright-core/src/client/browserContext.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { Frame } from './frame';
3030
import { HarRouter } from './harRouter';
3131
import * as network from './network';
3232
import { BindingCall, Page } from './page';
33+
import { RC } from './rc';
3334
import { Tracing } from './tracing';
3435
import { Waiter } from './waiter';
3536
import { WebError } from './webError';
@@ -68,6 +69,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
6869
private _closedPromise: Promise<void>;
6970
readonly _options: channels.BrowserNewContextParams;
7071

72+
readonly rc: RC;
7173
readonly request: APIRequestContext;
7274
readonly tracing: Tracing;
7375
readonly clock: Clock;
@@ -93,6 +95,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
9395
super(parent, type, guid, initializer);
9496
this._options = initializer.options;
9597
this._timeoutSettings = new TimeoutSettings(this._platform);
98+
this.rc = new RC(this._channel);
9699
this.tracing = Tracing.from(initializer.tracing);
97100
this.request = APIRequestContext.from(initializer.requestContext);
98101
this.request._timeoutSettings = this._timeoutSettings;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type * as api from '../../types/types';
18+
import type * as channels from '@protocol/channels';
19+
20+
export class RC implements api.RC {
21+
private _channel: channels.BrowserContextChannel;
22+
23+
constructor(channel: channels.BrowserContextChannel) {
24+
this._channel = channel;
25+
}
26+
27+
async startHttp(options: { size?: { width: number, height: number }, port?: number, host?: string } = {}): Promise<{ url: string }> {
28+
return await this._channel.rcStartHttp(options);
29+
}
30+
31+
async stopHttp(): Promise<void> {
32+
await this._channel.rcStopHttp();
33+
}
34+
}

packages/playwright-core/src/protocol/validator.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,6 +1157,19 @@ scheme.BrowserContextClockSetSystemTimeParams = tObject({
11571157
timeString: tOptional(tString),
11581158
});
11591159
scheme.BrowserContextClockSetSystemTimeResult = tOptional(tObject({}));
1160+
scheme.BrowserContextRcStartHttpParams = tObject({
1161+
size: tOptional(tObject({
1162+
width: tInt,
1163+
height: tInt,
1164+
})),
1165+
port: tOptional(tInt),
1166+
host: tOptional(tString),
1167+
});
1168+
scheme.BrowserContextRcStartHttpResult = tObject({
1169+
url: tString,
1170+
});
1171+
scheme.BrowserContextRcStopHttpParams = tOptional(tObject({}));
1172+
scheme.BrowserContextRcStopHttpResult = tOptional(tObject({}));
11601173
scheme.PageInitializer = tObject({
11611174
mainFrame: tChannel(['Frame']),
11621175
viewportSize: tOptional(tObject({

packages/playwright-core/src/server/browserContext.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import { Selectors } from './selectors';
3737
import { Tracing } from './trace/recorder/tracing';
3838
import * as rawStorageSource from '../generated/storageScriptSource';
3939

40+
import { RemoteControl } from './remoteControl';
41+
4042
import type { Artifact } from './artifact';
4143
import type { Browser, BrowserOptions } from './browser';
4244
import type { ConsoleMessage } from './console';
@@ -64,6 +66,8 @@ const BrowserContextEvent = {
6466
RequestContinued: 'requestcontinued',
6567
BeforeClose: 'beforeclose',
6668
RecorderEvent: 'recorderevent',
69+
PageClosed: 'pageclosed',
70+
InternalFrameNavigatedToNewDocument: 'internalframenavigatedtonewdocument',
6771
} as const;
6872

6973
export type BrowserContextEventMap = {
@@ -80,6 +84,8 @@ export type BrowserContextEventMap = {
8084
[BrowserContextEvent.RequestContinued]: [request: network.Request];
8185
[BrowserContextEvent.BeforeClose]: [];
8286
[BrowserContextEvent.RecorderEvent]: [event: { event: 'actionAdded' | 'actionUpdated' | 'signalAdded', data: any, page: Page, code: string }];
87+
[BrowserContextEvent.PageClosed]: [page: Page];
88+
[BrowserContextEvent.InternalFrameNavigatedToNewDocument]: [frame: frames.Frame, page: Page];
8389
};
8490

8591
export abstract class BrowserContext<EM extends EventMap = EventMap> extends SdkObject<BrowserContextEventMap | EM> {
@@ -114,6 +120,7 @@ export abstract class BrowserContext<EM extends EventMap = EventMap> extends Sdk
114120
private _playwrightBindingExposed?: Promise<void>;
115121
readonly dialogManager: DialogManager;
116122
private _consoleApiExposed = false;
123+
private _remoteControl: RemoteControl | null = null;
117124

118125
constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) {
119126
super(browser, 'browser-context');
@@ -509,6 +516,23 @@ export abstract class BrowserContext<EM extends EventMap = EventMap> extends Sdk
509516
await this.doUpdateRequestInterception();
510517
}
511518

519+
async startRemoteControl(options: { size?: types.Size, port?: number, host?: string } = {}): Promise<string> {
520+
if (this._remoteControl)
521+
throw new Error('Remote control is already running');
522+
const size = validateVideoSize(options.size, undefined);
523+
this._remoteControl = new RemoteControl(this);
524+
const url = await this._remoteControl.start({ width: size.width, height: size.height, quality: 90, port: options.port, host: options.host });
525+
return url;
526+
}
527+
528+
async stopRemoteControl(): Promise<void> {
529+
if (!this._remoteControl)
530+
throw new Error('Remote control is not running');
531+
const remoteControl = this._remoteControl;
532+
this._remoteControl = null;
533+
await remoteControl.stop();
534+
}
535+
512536
isClosingOrClosed() {
513537
return this._closedStatus !== 'open';
514538
}
@@ -532,6 +556,11 @@ export abstract class BrowserContext<EM extends EventMap = EventMap> extends Sdk
532556
this.emit(BrowserContext.Events.BeforeClose);
533557
this._closedStatus = 'closing';
534558

559+
if (this._remoteControl) {
560+
await this._remoteControl.stop();
561+
this._remoteControl = null;
562+
}
563+
535564
for (const harRecorder of this._harRecorders.values())
536565
await harRecorder.flush();
537566
await this.tracing.flush();

0 commit comments

Comments
 (0)