Skip to content

Commit 3fdbc06

Browse files
authored
chore: browser._launchServer (#37031)
1 parent 58cab06 commit 3fdbc06

File tree

9 files changed

+90
-18
lines changed

9 files changed

+90
-18
lines changed

packages/playwright-core/src/browserServerImpl.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import * as validatorPrimitives from './protocol/validatorPrimitives';
2626
import { ProgressController } from './server/progress';
2727

2828
import type { BrowserServer, BrowserServerLauncher } from './client/browserType';
29-
import type { LaunchServerOptions, Logger, Env } from './client/types';
29+
import type { LaunchOptions, LaunchServerOptions, Logger, Env } from './client/types';
3030
import type { ProtocolLogger } from './server/types';
3131
import type { WebSocketEventEmitter } from './utilsBundle';
3232
import type { Browser } from './server/browser';
@@ -38,7 +38,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
3838
this._browserName = browserName;
3939
}
4040

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

81+
return this.launchServerOnExistingBrowser(browser, options);
82+
}
83+
84+
async launchServerOnExistingBrowser(browser: Browser, options: LaunchServerOptions): Promise<BrowserServer> {
8185
const path = options.wsPath ? (options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`) : `/${createGuid()}`;
8286

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

8791
// 3. Return the BrowserServer interface

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { mkdirIfNeeded } from './fileUtils';
2424

2525
import type { BrowserType } from './browserType';
2626
import type { Page } from './page';
27-
import type { BrowserContextOptions, LaunchOptions, Logger } from './types';
27+
import type { BrowserContextOptions, LaunchOptions, LaunchServerOptions, Logger } from './types';
2828
import type * as api from '../../types/types';
2929
import type * as channels from '@protocol/channels';
3030

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

149+
async _launchServer(options: LaunchServerOptions = {}) {
150+
const serverLauncher = this._browserType._serverLauncher;
151+
const browserImpl = this._connection.toImpl?.(this);
152+
if (!serverLauncher || !browserImpl)
153+
throw new Error('Launching server is not supported');
154+
return await serverLauncher.launchServerOnExistingBrowser(browserImpl, {
155+
_sharedBrowser: true,
156+
...options,
157+
});
158+
}
159+
149160
async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
150161
this._path = options.path;
151162
await this._channel.startTracing({ ...options, page: page ? page._channel : undefined });

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ import type { ConnectOptions, LaunchOptions, LaunchPersistentContextOptions, Lau
3131
import type * as api from '../../types/types';
3232
import type * as channels from '@protocol/channels';
3333
import type { ChildProcess } from 'child_process';
34+
import type { Browser as BrowserImpl } from '../server/browser';
3435

3536
export interface BrowserServerLauncher {
36-
launchServer(options?: LaunchServerOptions): Promise<api.BrowserServer>;
37+
launchServer(options?: LaunchOptions & LaunchServerOptions): Promise<api.BrowserServer>;
38+
launchServerOnExistingBrowser(browser: BrowserImpl, options?: LaunchServerOptions): Promise<api.BrowserServer>;
3739
}
3840

3941
// This is here just for api generation and checking.

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,12 @@ export type ConnectOptions = {
105105
timeout?: number,
106106
logger?: Logger,
107107
};
108-
export type LaunchServerOptions = LaunchOptions & {
108+
export type LaunchServerOptions = {
109109
host?: string,
110110
port?: number,
111111
wsPath?: string,
112+
_debugController?: boolean;
113+
_sharedBrowser?: boolean;
112114
};
113115

114116
export type LaunchAndroidServerOptions = {

packages/playwright-core/src/remote/playwrightServer.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type ServerOptions = {
3939
preLaunchedBrowser?: Browser;
4040
preLaunchedAndroidDevice?: AndroidDevice;
4141
preLaunchedSocksProxy?: SocksProxy;
42+
debugController?: boolean;
4243
};
4344

4445
export class PlaywrightServer {
@@ -106,6 +107,19 @@ export class PlaywrightServer {
106107
const allowFSPaths = isExtension;
107108
launchOptions = filterLaunchOptions(launchOptions, allowFSPaths);
108109

110+
if (url.searchParams.has('debug-controller')) {
111+
if (!(this._options.debugController || isExtension))
112+
throw new Error('Debug controller is not enabled');
113+
return new PlaywrightConnection(
114+
controllerSemaphore,
115+
ws,
116+
true,
117+
this._playwright,
118+
async () => { throw new Error('shouldnt be used'); },
119+
id,
120+
);
121+
}
122+
109123
if (isExtension) {
110124
const connectFilter = url.searchParams.get('connect');
111125
if (connectFilter) {
@@ -121,16 +135,6 @@ export class PlaywrightServer {
121135
);
122136
}
123137

124-
if (url.searchParams.has('debug-controller')) {
125-
return new PlaywrightConnection(
126-
controllerSemaphore,
127-
ws,
128-
true,
129-
this._playwright,
130-
async () => { throw new Error('shouldnt be used'); },
131-
id,
132-
);
133-
}
134138
return new PlaywrightConnection(
135139
reuseBrowserSemaphore,
136140
ws,

packages/playwright-core/src/server/utils/wsServer.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,13 @@ export class WSServer {
106106
const url = new URL('http://localhost' + (request.url || ''));
107107
const id = String(++lastConnectionId);
108108
debugLogger.log('server', `[${id}] serving connection: ${request.url}`);
109-
const connection = this._delegate.onConnection(request, url, ws, id);
110-
(ws as any)[kConnectionSymbol] = connection;
109+
try {
110+
const connection = this._delegate.onConnection(request, url, ws, id);
111+
(ws as any)[kConnectionSymbol] = connection;
112+
} catch (error) {
113+
debugLogger.log('server', `[${id}] connection error: ${error}`);
114+
ws.close(1011, String(error));
115+
}
111116
});
112117

113118
return wsEndpoint;

tests/config/debugControllerBackend.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ export class Backend extends EventEmitter {
153153
}
154154

155155
private _send(method: string, params: any = {}): Promise<any> {
156+
if (this._transport.isClosed())
157+
throw new Error('Transport is closed');
156158
return new Promise((fulfill, reject) => {
157159
const id = ++Backend._lastId;
158160
const command = { id, guid: 'DebugController', method, params, metadata: {} };

tests/library/browsertype-connect.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,19 @@ test.describe('launchServer only', () => {
10461046
const browser = await connect(remoteServer.wsEndpoint()) as any;
10471047
await expect(browser._parent.launch({ timeout: 0 })).rejects.toThrowError('Launching more browsers is not allowed.');
10481048
});
1049+
1050+
test('should work with existing browser', async ({ connect, browserType }) => {
1051+
// can't use browser fixture because it's shared across the worker, launching a server on that would infect other tests
1052+
const browser = await browserType.launch();
1053+
const page = await browser.newPage();
1054+
await page.setContent('hello world');
1055+
const server = await (browser as any)._launchServer();
1056+
const secondBrowser = await connect(server.wsEndpoint());
1057+
const secondPage = secondBrowser.contexts()[0].pages()[0];
1058+
expect(await secondPage.content()).toContain('hello world');
1059+
await server.close();
1060+
await browser.close();
1061+
});
10491062
});
10501063

10511064
test('should refuse connecting when versions do not match', async ({ connect, childProcess }) => {

tests/library/debug-controller.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,3 +360,32 @@ test('should report error in aria template', async ({ backend }) => {
360360
const error = await backend.highlight({ ariaTemplate: `- button "Submit` }).catch(e => e);
361361
expect(error.message).toContain('Unterminated string:');
362362
});
363+
364+
test('should work with browser._launchServer', async ({ browser }) => {
365+
const server = await (browser as any)._launchServer({ _debugController: true });
366+
367+
const backend = new Backend();
368+
const connectionString = new URL(server.wsEndpoint());
369+
connectionString.searchParams.set('debug-controller', '');
370+
await backend.connect(connectionString.toString());
371+
await backend.initialize();
372+
await backend.channel.setReportStateChanged({ enabled: true });
373+
const pageCounts: number[] = [];
374+
backend.channel.on('stateChanged', event => pageCounts.push(event.pageCount));
375+
376+
const page = await browser.newPage();
377+
await page.close();
378+
expect(pageCounts).toEqual([1, 0]);
379+
});
380+
381+
test('should not work with browser._launchServer(_debugController: false)', async ({ browser }) => {
382+
const server = await (browser as any)._launchServer({ _debugController: false });
383+
384+
const backend = new Backend();
385+
const connectionString = new URL(server.wsEndpoint());
386+
connectionString.searchParams.set('debug-controller', '');
387+
await expect(async () => {
388+
await backend.connect(connectionString.toString());
389+
await backend.initialize();
390+
}).rejects.toThrow();
391+
});

0 commit comments

Comments
 (0)