Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests_mcp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ env:

jobs:
test_mcp:
name: ${{ matrix.os }}
name: ${{ matrix.os }} - ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
# Used for authentication of flakiness upload
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy:
Expand Down
16 changes: 8 additions & 8 deletions docs/src/pom.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,13 @@ test('getting started should contain table of contents', async ({ page }) => {
await playwrightDev.getStarted();
await expect(playwrightDev.tocList).toHaveText([
`How to install Playwright`,
`What's Installed`,
`What's installed`,
`How to run the example test`,
`How to open the HTML test report`,
`Write tests using web first assertions, page fixtures and locators`,
`Run single test, multiple tests, headed mode`,
`Write tests using web-first assertions, fixtures and locators`,
`Run single or multiple tests; headed mode`,
`Generate tests with Codegen`,
`See a trace of your tests`
`View a trace of your tests`,
]);
});

Expand All @@ -122,13 +122,13 @@ await playwrightDev.goto();
await playwrightDev.getStarted();
await expect(playwrightDev.tocList).toHaveText([
`How to install Playwright`,
`What's Installed`,
`What's installed`,
`How to run the example test`,
`How to open the HTML test report`,
`Write tests using web first assertions, page fixtures and locators`,
`Run single test, multiple tests, headed mode`,
`Write tests using web-first assertions, fixtures and locators`,
`Run single or multiple tests; headed mode`,
`Generate tests with Codegen`,
`See a trace of your tests`
`View a trace of your tests`,
]);
```

Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"./lib/outofprocess": "./lib/outofprocess.js",
"./lib/cli/program": "./lib/cli/program.js",
"./lib/cli/client/program": "./lib/cli/client/program.js",
"./lib/client/connect": "./lib/client/connect.js",
"./lib/mcpBundle": "./lib/mcpBundle.js",
"./lib/mcp/exports": "./lib/mcp/exports.js",
"./lib/tools/exports": "./lib/tools/exports.js",
Expand Down
3 changes: 3 additions & 0 deletions packages/playwright-core/src/DEPS.list
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ protocol/
utils/
utils/isomorphic
server/utils

[serverRegistry.ts]
"strict"
1 change: 1 addition & 0 deletions packages/playwright-core/src/cli/client/DEPS.list
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"strict"
./session.ts
./registry.ts
../../serverRegistry.ts

[session.ts]
"strict"
Expand Down
38 changes: 36 additions & 2 deletions packages/playwright-core/src/cli/client/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ import os from 'os';
import path from 'path';
import { createClientInfo, Registry, resolveSessionName } from './registry';
import { Session, renderResolvedConfig } from './session';
import { serverRegistry } from '../../serverRegistry';

import type { Config } from '../../mcp/config.d';
import type { ClientInfo, SessionFile } from './registry';
import type { BrowserDescriptor } from '../../serverRegistry';

type MinimistArgs = {
_: string[];
Expand Down Expand Up @@ -336,12 +338,24 @@ async function killAllDaemons(): Promise<void> {
async function listSessions(registry: Registry, clientInfo: ClientInfo, all: boolean): Promise<void> {
if (all) {
const entries = registry.entryMap();
if (entries.size === 0) {
const serverEntries = await serverRegistry.list({ gc: true });
if (entries.size === 0 && serverEntries.size === 0) {
console.log('No browsers found.');
return;
}

if (entries.size)
console.log('### Browsers');
for (const [workspace, list] of entries)
await gcAndPrintSessions(clientInfo, list.map(entry => new Session(entry)), `${workspace}:`);
await gcAndPrintSessions(clientInfo, list.map(entry => new Session(entry)), `${path.relative(process.cwd(), workspace) || '/'}:`);

if (serverEntries.size) {
if (entries.size)
console.log('');
console.log('### Browser servers available for attach');
}
for (const [workspace, list] of serverEntries)
await gcAndPrintBrowserSessions(workspace, list);
} else {
console.log('### Browsers');
const entries = registry.entries(clientInfo);
Expand Down Expand Up @@ -377,6 +391,26 @@ async function gcAndPrintSessions(clientInfo: ClientInfo, sessions: Session[], h
console.log(' (no browsers)');
}

async function gcAndPrintBrowserSessions(workspace: string, list: BrowserDescriptor[]) {
if (!list.length)
return;

if (workspace)
console.log(`${path.relative(process.cwd(), workspace) || '/'}:`);

for (const descriptor of list) {
const text: string[] = [];
text.push(`- browser "${descriptor.title}":`);
text.push(` - browser: ${descriptor.browser.browserName}`);
text.push(` - version: v${descriptor.playwrightVersion}`);
text.push(` - run \`playwright-cli open --attach "${descriptor.title}"\` to attach`);
console.log(text.join('\n'));
}

if (!list.length)
console.log(' (no browsers)');
}

async function renderSessionStatus(clientInfo: ClientInfo, session: Session) {
const text: string[] = [];
const config = session.config;
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-core/src/cli/client/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ export class Session {
args.push(`--profile=${cliArgs.profile}`);
if (cliArgs.config)
args.push(`--config=${cliArgs.config}`);
if (cliArgs.attach)
args.push(`--attach=${cliArgs.attach}`);

const child = spawn(process.execPath, args, {
detached: true,
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/cli/daemon/DEPS.list
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
../../mcpBundle.ts
../../mcp/
../../server/utils/
../../serverRegistry.ts
1 change: 1 addition & 0 deletions packages/playwright-core/src/cli/daemon/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const open = declareCommand({
headed: z.boolean().optional().describe('Run browser in headed mode'),
persistent: z.boolean().optional().describe('Use persistent browser profile'),
profile: z.string().optional().describe('Use persistent browser profile, store profile in specified directory.'),
attach: z.string().optional().describe('Attach to a running Playwright browser by name or endpoint'),
}),
toolName: ({ url }) => url ? 'browser_navigate' : 'browser_snapshot',
toolParams: ({ url }) => ({ url: url || 'about:blank' }),
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-core/src/cli/daemon/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function decorateCLICommand(command: Command, version: string) {
.option('--persistent', 'use a persistent browser context')
.option('--profile <path>', 'path to the user data dir')
.option('--config <path>', 'path to the config file')
.option('--attach <name-or-endpoint>', 'attach to a running Playwright browser by name or endpoint')

.action(async (sessionName: string, options: any) => {
setupExitWatchdog();
Expand Down Expand Up @@ -84,6 +85,7 @@ export async function resolveCLIConfig(clientInfo: ClientInfo, sessionName: stri
outputMode: 'file',
snapshotMode: 'full',
});
daemonOverrides.browser!.remoteEndpoint = options.attach;

const envOverrides = configUtils.configFromEnv();
const configFile = envOverrides.configFile ?? daemonOverrides.configFile;
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/client/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { TimeoutSettings } from './timeoutSettings';
import { isRegExp, isString } from '../utils/isomorphic/rtti';
import { monotonicTime } from '../utils/isomorphic/time';
import { raceAgainstDeadline } from '../utils/isomorphic/timeoutRunner';
import { connectOverWebSocket } from './webSocket';
import { connectToEndpoint } from './connect';

import type { Page } from './page';
import type * as types from './types';
Expand Down Expand Up @@ -71,7 +71,7 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
const deadline = options.timeout ? monotonicTime() + options.timeout : 0;
const headers = { 'x-playwright-browser': 'android', ...options.headers };
const connectParams: channels.LocalUtilsConnectParams = { endpoint, headers, slowMo: options.slowMo, timeout: options.timeout || 0 };
const connection = await connectOverWebSocket(this._connection, connectParams);
const connection = await connectToEndpoint(this._connection, connectParams);

let device: AndroidDevice;
connection.on('close', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-core/src/client/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
_options: LaunchOptions = {};
_userDataDir: string | undefined;
readonly _name: string;
readonly _browserName: 'chromium' | 'webkit' | 'firefox';
private _path: string | undefined;
_closeReason: string | undefined;

Expand All @@ -47,6 +48,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.BrowserInitializer) {
super(parent, type, guid, initializer);
this._name = initializer.name;
this._browserName = initializer.browserName;
this._channel.on('context', ({ context }) => this._didCreateContext(BrowserContext.from(context)));
this._channel.on('close', () => this._didClose());
this._closedPromise = new Promise(f => this.once(Events.Browser.Disconnected, f));
Expand Down
58 changes: 6 additions & 52 deletions packages/playwright-core/src/client/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,9 @@ import { Browser } from './browser';
import { BrowserContext, prepareBrowserContextParams } from './browserContext';
import { ChannelOwner } from './channelOwner';
import { envObjectToArray } from './clientHelper';
import { Events } from './events';
import { assert } from '../utils/isomorphic/assert';
import { headersObjectToArray } from '../utils/isomorphic/headers';
import { monotonicTime } from '../utils/isomorphic/time';
import { raceAgainstDeadline } from '../utils/isomorphic/timeoutRunner';
import { connectOverWebSocket } from './webSocket';
import { connectToBrowser } from './connect';
import { TimeoutSettings } from './timeoutSettings';

import type { Playwright } from './playwright';
Expand Down Expand Up @@ -123,62 +120,19 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple

connect(options: api.ConnectOptions & { wsEndpoint: string }): Promise<Browser>;
connect(endpoint: string, options?: api.ConnectOptions): Promise<Browser>;
async connect(optionsOrEndpoint: string | (api.ConnectOptions & { wsEndpoint?: string, pipeName?: string }), options?: api.ConnectOptions): Promise<Browser>{
async connect(optionsOrEndpoint: string | (api.ConnectOptions & { wsEndpoint?: string }), options?: api.ConnectOptions): Promise<Browser>{
if (typeof optionsOrEndpoint === 'string')
return await this._connect({ ...options, endpoint: optionsOrEndpoint });
assert(optionsOrEndpoint.wsEndpoint, 'options.wsEndpoint is required');
return await this._connect(optionsOrEndpoint);
return await this._connect({ ...options, endpoint: optionsOrEndpoint.wsEndpoint });
}

async _connect(params: ConnectOptions): Promise<Browser> {
const logger = params.logger;
return await this._wrapApiCall(async () => {
const deadline = params.timeout ? monotonicTime() + params.timeout : 0;
const headers = { 'x-playwright-browser': this.name(), ...params.headers };
const connectParams: channels.LocalUtilsConnectParams = {
endpoint: params.endpoint!,
headers,
exposeNetwork: params.exposeNetwork,
slowMo: params.slowMo,
timeout: params.timeout || 0,
};
if ((params as any).__testHookRedirectPortForwarding)
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding;
const connection = await connectOverWebSocket(this._connection, connectParams);
let browser: Browser;
connection.on('close', () => {
// Emulate all pages, contexts and the browser closing upon disconnect.
for (const context of browser?.contexts() || []) {
for (const page of context.pages())
page._onClose();
context._onClose();
}
setTimeout(() => browser?._didClose(), 0);
});

const result = await raceAgainstDeadline(async () => {
// For tests.
if ((params as any).__testHookBeforeCreateBrowser)
await (params as any).__testHookBeforeCreateBrowser();

const playwright = await connection!.initializePlaywright();
if (!playwright._initializer.preLaunchedBrowser) {
connection.close();
throw new Error('Malformed endpoint. Did you use BrowserType.launchServer method?');
}
playwright.selectors = this._playwright.selectors;
browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
browser._connectToBrowserType(this, {}, logger);
browser._shouldCloseConnectionOnClose = true;
browser.on(Events.Browser.Disconnected, () => connection.close());
return browser;
}, deadline);
if (!result.timedOut) {
return result.result;
} else {
connection.close();
throw new Error(`Timeout ${params.timeout}ms exceeded`);
}
const browser = await connectToBrowser(this._playwright, { browserName: this.name(), ...params });
browser._connectToBrowserType(this, {}, logger);
return browser;
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,69 @@
* limitations under the License.
*/

import { monotonicTime } from '../utils/isomorphic/time';
import { raceAgainstDeadline } from '../utils/isomorphic/timeoutRunner';
import { Browser } from './browser';
import { ChannelOwner } from './channelOwner';
import { Connection } from './connection';
import { Events } from './events';

import type { HeadersArray } from './types';
import type * as playwright from '../..';
import type { Playwright } from './playwright';
import type { ConnectOptions, HeadersArray } from './types';
import type * as channels from '@protocol/channels';
import type { BrowserDescriptor } from '../serverRegistry';

export async function connectToBrowser(playwright: Playwright, params: ConnectOptions): Promise<Browser> {
const deadline = params.timeout ? monotonicTime() + params.timeout : 0;
const nameParam = params.browserName ? { 'x-playwright-browser': params.browserName } : {};
const headers = { ...nameParam, ...params.headers };
const connectParams: channels.LocalUtilsConnectParams = {
endpoint: params.endpoint!,
headers,
exposeNetwork: params.exposeNetwork,
slowMo: params.slowMo,
timeout: params.timeout || 0,
};
if ((params as any).__testHookRedirectPortForwarding)
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding;
const connection = await connectToEndpoint(playwright._connection, connectParams);
let browser: Browser;
connection.on('close', () => {
// Emulate all pages, contexts and the browser closing upon disconnect.
for (const context of browser?.contexts() || []) {
for (const page of context.pages())
page._onClose();
context._onClose();
}
setTimeout(() => browser?._didClose(), 0);
});

const result = await raceAgainstDeadline(async () => {
// For tests.
if ((params as any).__testHookBeforeCreateBrowser)
await (params as any).__testHookBeforeCreateBrowser();

const playwright = await connection!.initializePlaywright();
if (!playwright._initializer.preLaunchedBrowser) {
connection.close();
throw new Error('Malformed endpoint. Did you use BrowserType.launchServer method?');
}
playwright.selectors = playwright.selectors;
browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
browser._shouldCloseConnectionOnClose = true;
browser.on(Events.Browser.Disconnected, () => connection.close());
return browser;
}, deadline);
if (!result.timedOut) {
return result.result;
} else {
connection.close();
throw new Error(`Timeout ${params.timeout}ms exceeded`);
}
}

export async function connectOverWebSocket(parentConnection: Connection, params: channels.LocalUtilsConnectParams): Promise<Connection> {
export async function connectToEndpoint(parentConnection: Connection, params: channels.LocalUtilsConnectParams): Promise<Connection> {
const localUtils = parentConnection.localUtils();
const transport = localUtils ? new JsonPipeTransport(localUtils) : new WebSocketTransport();
const connectHeaders = await transport.connect(params);
Expand All @@ -45,6 +101,14 @@ export async function connectOverWebSocket(parentConnection: Connection, params:
return connection;
}

export async function connectToBrowserAcrossVersions(descriptor: BrowserDescriptor): Promise<playwright.Browser> {
const pw = require(descriptor.playwrightLib);
const params: ConnectOptions = { endpoint: descriptor.pipeName! };
const browser = await connectToBrowser(pw, params);
browser._connectToBrowserType(pw[descriptor.browser.browserName], {}, undefined);
return browser;
}

interface Transport {
connect(params: channels.LocalUtilsConnectParams): Promise<HeadersArray>;
send(message: any): Promise<void>;
Expand Down
3 changes: 2 additions & 1 deletion packages/playwright-core/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ export type LaunchOptions = Omit<channels.BrowserTypeLaunchOptions, 'ignoreAllDe
export type LaunchPersistentContextOptions = Omit<LaunchOptions & BrowserContextOptions, 'storageState'>;

export type ConnectOptions = {
endpoint?: string;
endpoint: string;
browserName?: string;
headers?: { [key: string]: string; };
exposeNetwork?: string;
slowMo?: number;
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/mcp/DEPS.list
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
../server/utils/
../client/
../cli/
../serverRegistry.ts
Loading
Loading