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
10 changes: 7 additions & 3 deletions packages/playwright-core/src/browserServerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import * as validatorPrimitives from './protocol/validatorPrimitives';
import { ProgressController } from './server/progress';

import type { BrowserServer, BrowserServerLauncher } from './client/browserType';
import type { LaunchServerOptions, Logger, Env } from './client/types';
import type { LaunchOptions, LaunchServerOptions, Logger, Env } from './client/types';
import type { ProtocolLogger } from './server/types';
import type { WebSocketEventEmitter } from './utilsBundle';
import type { Browser } from './server/browser';
Expand All @@ -38,7 +38,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
this._browserName = browserName;
}

async launchServer(options: LaunchServerOptions & { _sharedBrowser?: boolean, _userDataDir?: string } = {}): Promise<BrowserServer> {
async launchServer(options: LaunchOptions & LaunchServerOptions & { _userDataDir?: string } = {}): Promise<BrowserServer> {
const playwright = createPlaywright({ sdkLanguage: 'javascript', isServer: true });
// 1. Pre-launch the browser
const metadata = { id: '', startTime: 0, endTime: 0, type: 'Internal', method: '', params: {}, log: [], internal: true };
Expand Down Expand Up @@ -78,10 +78,14 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
throw e;
}

return this.launchServerOnExistingBrowser(browser, options);
}

async launchServerOnExistingBrowser(browser: Browser, options: LaunchServerOptions): Promise<BrowserServer> {
const path = options.wsPath ? (options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`) : `/${createGuid()}`;

// 2. Start the server
const server = new PlaywrightServer({ mode: options._sharedBrowser ? 'launchServerShared' : 'launchServer', path, maxConnections: Infinity, preLaunchedBrowser: browser });
const server = new PlaywrightServer({ mode: options._sharedBrowser ? 'launchServerShared' : 'launchServer', path, maxConnections: Infinity, preLaunchedBrowser: browser, debugController: options._debugController });
const wsEndpoint = await server.listen(options.port, options.host);

// 3. Return the BrowserServer interface
Expand Down
13 changes: 12 additions & 1 deletion packages/playwright-core/src/client/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { mkdirIfNeeded } from './fileUtils';

import type { BrowserType } from './browserType';
import type { Page } from './page';
import type { BrowserContextOptions, LaunchOptions, Logger } from './types';
import type { BrowserContextOptions, LaunchOptions, LaunchServerOptions, Logger } from './types';
import type * as api from '../../types/types';
import type * as channels from '@protocol/channels';

Expand Down Expand Up @@ -146,6 +146,17 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
return CDPSession.from((await this._channel.newBrowserCDPSession()).session);
}

async _launchServer(options: LaunchServerOptions = {}) {
const serverLauncher = this._browserType._serverLauncher;
const browserImpl = this._connection.toImpl?.(this);
if (!serverLauncher || !browserImpl)
throw new Error('Launching server is not supported');
return await serverLauncher.launchServerOnExistingBrowser(browserImpl, {
_sharedBrowser: true,
...options,
});
}

async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
this._path = options.path;
await this._channel.startTracing({ ...options, page: page ? page._channel : undefined });
Expand Down
4 changes: 3 additions & 1 deletion packages/playwright-core/src/client/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ import type { ConnectOptions, LaunchOptions, LaunchPersistentContextOptions, Lau
import type * as api from '../../types/types';
import type * as channels from '@protocol/channels';
import type { ChildProcess } from 'child_process';
import type { Browser as BrowserImpl } from '../server/browser';

export interface BrowserServerLauncher {
launchServer(options?: LaunchServerOptions): Promise<api.BrowserServer>;
launchServer(options?: LaunchOptions & LaunchServerOptions): Promise<api.BrowserServer>;
launchServerOnExistingBrowser(browser: BrowserImpl, options?: LaunchServerOptions): Promise<api.BrowserServer>;
}

// This is here just for api generation and checking.
Expand Down
4 changes: 3 additions & 1 deletion packages/playwright-core/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,12 @@ export type ConnectOptions = {
timeout?: number,
logger?: Logger,
};
export type LaunchServerOptions = LaunchOptions & {
export type LaunchServerOptions = {
host?: string,
port?: number,
wsPath?: string,
_debugController?: boolean;
_sharedBrowser?: boolean;
};

export type LaunchAndroidServerOptions = {
Expand Down
24 changes: 14 additions & 10 deletions packages/playwright-core/src/remote/playwrightServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type ServerOptions = {
preLaunchedBrowser?: Browser;
preLaunchedAndroidDevice?: AndroidDevice;
preLaunchedSocksProxy?: SocksProxy;
debugController?: boolean;
};

export class PlaywrightServer {
Expand Down Expand Up @@ -106,6 +107,19 @@ export class PlaywrightServer {
const allowFSPaths = isExtension;
launchOptions = filterLaunchOptions(launchOptions, allowFSPaths);

if (url.searchParams.has('debug-controller')) {
if (!(this._options.debugController || isExtension))
throw new Error('Debug controller is not enabled');
return new PlaywrightConnection(
controllerSemaphore,
ws,
true,
this._playwright,
async () => { throw new Error('shouldnt be used'); },
id,
);
}

if (isExtension) {
const connectFilter = url.searchParams.get('connect');
if (connectFilter) {
Expand All @@ -121,16 +135,6 @@ export class PlaywrightServer {
);
}

if (url.searchParams.has('debug-controller')) {
return new PlaywrightConnection(
controllerSemaphore,
ws,
true,
this._playwright,
async () => { throw new Error('shouldnt be used'); },
id,
);
}
return new PlaywrightConnection(
reuseBrowserSemaphore,
ws,
Expand Down
9 changes: 7 additions & 2 deletions packages/playwright-core/src/server/utils/wsServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,13 @@ export class WSServer {
const url = new URL('http://localhost' + (request.url || ''));
const id = String(++lastConnectionId);
debugLogger.log('server', `[${id}] serving connection: ${request.url}`);
const connection = this._delegate.onConnection(request, url, ws, id);
(ws as any)[kConnectionSymbol] = connection;
try {
const connection = this._delegate.onConnection(request, url, ws, id);
(ws as any)[kConnectionSymbol] = connection;
} catch (error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lovely! I was about to comment that we should handle exceptions 😄

debugLogger.log('server', `[${id}] connection error: ${error}`);
ws.close(1011, String(error));
}
});

return wsEndpoint;
Expand Down
2 changes: 2 additions & 0 deletions tests/config/debugControllerBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ export class Backend extends EventEmitter {
}

private _send(method: string, params: any = {}): Promise<any> {
if (this._transport.isClosed())
throw new Error('Transport is closed');
return new Promise((fulfill, reject) => {
const id = ++Backend._lastId;
const command = { id, guid: 'DebugController', method, params, metadata: {} };
Expand Down
13 changes: 13 additions & 0 deletions tests/library/browsertype-connect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,19 @@ test.describe('launchServer only', () => {
const browser = await connect(remoteServer.wsEndpoint()) as any;
await expect(browser._parent.launch({ timeout: 0 })).rejects.toThrowError('Launching more browsers is not allowed.');
});

test('should work with existing browser', async ({ connect, browserType }) => {
// can't use browser fixture because it's shared across the worker, launching a server on that would infect other tests
const browser = await browserType.launch();
const page = await browser.newPage();
await page.setContent('hello world');
const server = await (browser as any)._launchServer();
const secondBrowser = await connect(server.wsEndpoint());
const secondPage = secondBrowser.contexts()[0].pages()[0];
expect(await secondPage.content()).toContain('hello world');
await server.close();
await browser.close();
});
});

test('should refuse connecting when versions do not match', async ({ connect, childProcess }) => {
Expand Down
29 changes: 29 additions & 0 deletions tests/library/debug-controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,32 @@ test('should report error in aria template', async ({ backend }) => {
const error = await backend.highlight({ ariaTemplate: `- button "Submit` }).catch(e => e);
expect(error.message).toContain('Unterminated string:');
});

test('should work with browser._launchServer', async ({ browser }) => {
const server = await (browser as any)._launchServer({ _debugController: true });

const backend = new Backend();
const connectionString = new URL(server.wsEndpoint());
connectionString.searchParams.set('debug-controller', '');
await backend.connect(connectionString.toString());
await backend.initialize();
await backend.channel.setReportStateChanged({ enabled: true });
const pageCounts: number[] = [];
backend.channel.on('stateChanged', event => pageCounts.push(event.pageCount));

const page = await browser.newPage();
await page.close();
expect(pageCounts).toEqual([1, 0]);
});

test('should not work with browser._launchServer(_debugController: false)', async ({ browser }) => {
const server = await (browser as any)._launchServer({ _debugController: false });

const backend = new Backend();
const connectionString = new URL(server.wsEndpoint());
connectionString.searchParams.set('debug-controller', '');
await expect(async () => {
await backend.connect(connectionString.toString());
await backend.initialize();
}).rejects.toThrow();
});
Loading