chore: move some playwright-wide options to be per browser (#36342)

This commit is contained in:
Dmitry Gozman 2025-06-18 08:28:33 +01:00 committed by GitHub
parent a7ff65cb8b
commit 8fcf838c37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 43 additions and 35 deletions

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { SocksProxy } from './server/utils/socksProxy';
import { PlaywrightServer } from './remote/playwrightServer';
import { helper } from './server/helper';
import { serverSideCallMetadata } from './server/instrumentation';
@ -41,10 +40,6 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
async launchServer(options: LaunchServerOptions & { _sharedBrowser?: boolean, _userDataDir?: string } = {}): Promise<BrowserServer> {
const playwright = createPlaywright({ sdkLanguage: 'javascript', isServer: true });
// TODO: enable socks proxy once ipv6 is supported.
const socksProxy = false ? new SocksProxy() : undefined;
playwright.options.socksProxyPort = await socksProxy?.listen(0);
// 1. Pre-launch the browser
const metadata = serverSideCallMetadata();
const validatorContext = {
@ -83,7 +78,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
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, preLaunchedSocksProxy: socksProxy });
const server = new PlaywrightServer({ mode: options._sharedBrowser ? 'launchServerShared' : 'launchServer', path, maxConnections: Infinity, preLaunchedBrowser: browser });
const wsEndpoint = await server.listen(options.port, options.host);
// 3. Return the BrowserServer interface
@ -96,7 +91,6 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
(browserServer as any)._disconnectForTest = () => server.close();
(browserServer as any)._userDataDirForTest = (browser as any)._userDataDirForTest;
browser.options.browserProcess.onclose = (exitCode, signal) => {
socksProxy?.close().catch(() => {});
server.close();
browserServer.emit('close', exitCode, signal);
};

View File

@ -107,9 +107,9 @@ export class PlaywrightConnection {
this._root = new RootDispatcher(this._dispatcherConnection, async (scope, options) => {
await startProfiling();
if (clientType === 'reuse-browser')
return await this._initReuseBrowsersMode(scope);
return await this._initReuseBrowsersMode(scope, options);
if (clientType === 'pre-launched-browser-or-android')
return this._preLaunched.browser ? await this._initPreLaunchedBrowserMode(scope) : await this._initPreLaunchedAndroidMode(scope);
return this._preLaunched.browser ? await this._initPreLaunchedBrowserMode(scope, options) : await this._initPreLaunchedAndroidMode(scope);
if (clientType === 'launch-browser')
return await this._initLaunchBrowserMode(scope, options);
throw new Error('Unsupported client type: ' + clientType);
@ -120,7 +120,7 @@ export class PlaywrightConnection {
debugLogger.log('server', `[${this._id}] engaged launch mode for "${this._options.browserName}"`);
const playwright = createPlaywright({ sdkLanguage: options.sdkLanguage, isServer: true });
const ownedSocksProxy = await this._createOwnedSocksProxy(playwright);
const ownedSocksProxy = await this._createOwnedSocksProxy();
let browserName = this._options.browserName;
if ('bidi' === browserName) {
if (this._options.launchOptions?.channel?.toLocaleLowerCase().includes('firefox'))
@ -129,6 +129,7 @@ export class PlaywrightConnection {
browserName = 'bidiChromium';
}
const browser = await playwright[browserName as 'chromium'].launch(serverSideCallMetadata(), this._options.launchOptions);
browser.options.sdkLanguage = options.sdkLanguage;
this._cleanups.push(async () => {
for (const browser of playwright.allBrowsers())
@ -142,7 +143,7 @@ export class PlaywrightConnection {
return new PlaywrightDispatcher(scope, playwright, { socksProxy: ownedSocksProxy, preLaunchedBrowser: browser });
}
private async _initPreLaunchedBrowserMode(scope: RootDispatcher) {
private async _initPreLaunchedBrowserMode(scope: RootDispatcher, options: channels.RootInitializeParams) {
debugLogger.log('server', `[${this._id}] engaged pre-launched (browser) mode`);
const playwright = this._preLaunched.playwright!;
@ -150,6 +151,7 @@ export class PlaywrightConnection {
this._preLaunched.socksProxy?.setPattern(this._options.socksProxyPattern);
const browser = this._preLaunched.browser!;
browser.options.sdkLanguage = options.sdkLanguage;
browser.on(Browser.Events.Disconnected, () => {
// Underlying browser did close for some reason - force disconnect the client.
this.close({ code: 1001, reason: 'Browser closed' });
@ -189,7 +191,7 @@ export class PlaywrightConnection {
return new DebugControllerDispatcher(this._dispatcherConnection, playwright.debugController);
}
private async _initReuseBrowsersMode(scope: RootDispatcher) {
private async _initReuseBrowsersMode(scope: RootDispatcher, options: channels.RootInitializeParams) {
// Note: reuse browser mode does not support socks proxy, because
// clients come and go, while the browser stays the same.
@ -222,6 +224,7 @@ export class PlaywrightConnection {
this.close({ code: 1001, reason: 'Browser closed' });
});
}
browser.options.sdkLanguage = options.sdkLanguage;
this._cleanups.push(async () => {
// Don't close the pages so that user could debug them,
@ -242,13 +245,15 @@ export class PlaywrightConnection {
return playwrightDispatcher;
}
private async _createOwnedSocksProxy(playwright: Playwright): Promise<SocksProxy | undefined> {
if (!this._options.socksProxyPattern)
private async _createOwnedSocksProxy(): Promise<SocksProxy | undefined> {
if (!this._options.socksProxyPattern) {
this._options.launchOptions.socksProxyPort = undefined;
return;
}
const socksProxy = new SocksProxy();
socksProxy.setPattern(this._options.socksProxyPattern);
playwright.options.socksProxyPort = await socksProxy.listen(0);
debugLogger.log('server', `[${this._id}] started socks proxy on port ${playwright.options.socksProxyPort}`);
this._options.launchOptions.socksProxyPort = await socksProxy.listen(0);
debugLogger.log('server', `[${this._id}] started socks proxy on port ${this._options.launchOptions.socksProxyPort}`);
this._cleanups.push(() => socksProxy.close());
return socksProxy;
}

View File

@ -144,14 +144,14 @@ export class BidiChromium extends BrowserType {
const proxyURL = new URL(proxy.server);
const isSocks = proxyURL.protocol === 'socks5:';
// https://www.chromium.org/developers/design-documents/network-settings
if (isSocks && !this.attribution.playwright.options.socksProxyPort) {
if (isSocks && !options.socksProxyPort) {
// https://www.chromium.org/developers/design-documents/network-stack/socks-proxy
chromeArguments.push(`--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${proxyURL.hostname}"`);
}
chromeArguments.push(`--proxy-server=${proxy.server}`);
const proxyBypassRules = [];
// https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578
if (this.attribution.playwright.options.socksProxyPort)
if (options.socksProxyPort)
proxyBypassRules.push('<-loopback>');
if (proxy.bypass)
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));

View File

@ -27,6 +27,7 @@ import type { ProxySettings } from './types';
import type { RecentLogsCollector } from './utils/debugLogger';
import type * as channels from '@protocol/channels';
import type { ChildProcess } from 'child_process';
import type { Language } from '../utils';
export interface BrowserProcess {
@ -52,6 +53,7 @@ export type BrowserOptions = {
browserLogsCollector: RecentLogsCollector,
slowMo?: number;
wsEndpoint?: string; // Only there when connected over web socket.
sdkLanguage?: Language;
originalLaunchOptions: types.LaunchOptions;
};
@ -84,6 +86,10 @@ export abstract class Browser extends SdkObject {
abstract version(): string;
abstract userAgent(): string;
sdkLanguage() {
return this.options.sdkLanguage || this.attribution.playwright.options.sdkLanguage;
}
async newContext(metadata: CallMetadata, options: types.BrowserContextOptions): Promise<BrowserContext> {
validateBrowserContextOptions(options, this.options);
let clientCertificatesProxy: ClientCertificatesProxy | undefined;

View File

@ -79,7 +79,7 @@ export abstract class BrowserType extends SdkObject {
return browser;
}
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { timeout: number, cdpPort?: number, internalIgnoreHTTPSErrors?: boolean }): Promise<BrowserContext> {
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { timeout: number, cdpPort?: number, internalIgnoreHTTPSErrors?: boolean, socksProxyPort?: number }): Promise<BrowserContext> {
const launchOptions = this._validateLaunchOptions(options);
const controller = new ProgressController(metadata, this);
const browser = await controller.run(async progress => {
@ -288,8 +288,8 @@ export abstract class BrowserType extends SdkObject {
headless = false;
if (downloadsPath && !path.isAbsolute(downloadsPath))
downloadsPath = path.join(process.cwd(), downloadsPath);
if (this.attribution.playwright.options.socksProxyPort)
proxy = { server: `socks5://127.0.0.1:${this.attribution.playwright.options.socksProxyPort}` };
if (options.socksProxyPort)
proxy = { server: `socks5://127.0.0.1:${options.socksProxyPort}` };
return { ...options, devtools, headless, downloadsPath, proxy };
}

View File

@ -331,14 +331,14 @@ export class Chromium extends BrowserType {
const proxyURL = new URL(proxy.server);
const isSocks = proxyURL.protocol === 'socks5:';
// https://www.chromium.org/developers/design-documents/network-settings
if (isSocks && !this.attribution.playwright.options.socksProxyPort) {
if (isSocks && !options.socksProxyPort) {
// https://www.chromium.org/developers/design-documents/network-stack/socks-proxy
chromeArguments.push(`--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${proxyURL.hostname}"`);
}
chromeArguments.push(`--proxy-server=${proxy.server}`);
const proxyBypassRules = [];
// https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578
if (this.attribution.playwright.options.socksProxyPort)
if (options.socksProxyPort)
proxyBypassRules.push('<-loopback>');
if (proxy.bypass)
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));

View File

@ -894,7 +894,7 @@ class FrameSession {
async _createVideoRecorder(screencastId: string, options: types.PageScreencastOptions): Promise<void> {
assert(!this._screencastId);
const ffmpegPath = registry.findExecutable('ffmpeg')!.executablePathOrDie(this._page.attribution.playwright.options.sdkLanguage);
const ffmpegPath = registry.findExecutable('ffmpeg')!.executablePathOrDie(this._page.browserContext._browser.sdkLanguage());
this._videoRecorder = await VideoRecorder.launch(this._crPage._page, ffmpegPath, options);
this._screencastId = screencastId;
}

View File

@ -87,7 +87,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
const selectorsRegistry = this.frame._page.browserContext.selectors();
for (const [name, { source }] of selectorsRegistry._engines)
customEngines.push({ name, source: `(${source})` });
const sdkLanguage = this.frame.attribution.playwright.options.sdkLanguage;
const sdkLanguage = this.frame._page.browserContext._browser.sdkLanguage();
const options: InjectedScriptOptions = {
isUnderTest: isUnderTest(),
sdkLanguage,

View File

@ -134,7 +134,7 @@ export class FrameSelectors {
for (const chunk of frameChunks) {
visitAllSelectorParts(chunk, (part, nested) => {
if (nested && part.name === 'internal:control' && part.body === 'enter-frame') {
const locator = asLocator(this.frame._page.attribution.playwright.options.sdkLanguage, selector);
const locator = asLocator(this.frame._page.browserContext._browser.sdkLanguage(), selector);
throw new InvalidSelectorError(`Frame locators are not allowed inside composite locators, while querying "${locator}"`);
}
});

View File

@ -1711,7 +1711,7 @@ export class Frame extends SdkObject {
}
private _asLocator(selector: string) {
return asLocator(this._page.attribution.playwright.options.sdkLanguage, selector);
return asLocator(this._page.browserContext._browser.sdkLanguage(), selector);
}
}

View File

@ -484,11 +484,11 @@ export class Page extends SdkObject {
}
if (handler.resolved) {
++this._locatorHandlerRunningCounter;
progress.log(` found ${asLocator(this.attribution.playwright.options.sdkLanguage, handler.selector)}, intercepting action to run the handler`);
progress.log(` found ${asLocator(this.browserContext._browser.sdkLanguage(), handler.selector)}, intercepting action to run the handler`);
const promise = handler.resolved.then(async () => {
progress.throwIfAborted();
if (!handler.noWaitAfter) {
progress.log(` locator handler has finished, waiting for ${asLocator(this.attribution.playwright.options.sdkLanguage, handler.selector)} to be hidden`);
progress.log(` locator handler has finished, waiting for ${asLocator(this.browserContext._browser.sdkLanguage(), handler.selector)} to be hidden`);
await this.mainFrame().waitForSelectorInternal(progress, handler.selector, false, { state: 'hidden' });
} else {
progress.log(` locator handler has finished`);

View File

@ -33,7 +33,6 @@ import type { CallMetadata } from './instrumentation';
import type { Page } from './page';
type PlaywrightOptions = {
socksProxyPort?: number;
sdkLanguage: Language;
isInternalPlaywright?: boolean;
isServer?: boolean;

View File

@ -63,7 +63,7 @@ export class ContextRecorder extends EventEmitter {
this._params = params;
this._delegate = delegate;
this._recorderSources = [];
const language = params.language || context.attribution.playwright.options.sdkLanguage;
const language = params.language || context._browser.sdkLanguage();
this.setOutput(language, params.outputFile);
// Make a copy of options to modify them later.

View File

@ -103,7 +103,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
}
private static async _open(recorder: IRecorder, inspectedContext: BrowserContext): Promise<IRecorderApp> {
const sdkLanguage = inspectedContext.attribution.playwright.options.sdkLanguage;
const sdkLanguage = inspectedContext._browser.sdkLanguage();
const headed = !!inspectedContext._browser.options.headful;
const recorderPlaywright = (require('../playwright').createPlaywright as typeof import('../playwright').createPlaywright)({ sdkLanguage: 'javascript', isInternalPlaywright: true });
const { context, page } = await launchApp(recorderPlaywright.chromium, {

View File

@ -109,7 +109,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
platform: process.platform,
wallTime: 0,
monotonicTime: 0,
sdkLanguage: context.attribution.playwright.options.sdkLanguage,
sdkLanguage: this._sdkLanguage(),
testIdAttributeName,
contextId: context.guid,
};
@ -122,6 +122,10 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
}
}
private _sdkLanguage() {
return this._context instanceof BrowserContext ? this._context._browser.sdkLanguage() : this._context.attribution.playwright.options.sdkLanguage;
}
async resetForReuse() {
// Discard previous chunk if any and ignore any errors there.
await this.stopChunk({ mode: 'discard' }).catch(() => {});
@ -136,7 +140,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
throw new Error('Tracing has been already started');
// Re-write for testing.
this._contextCreatedEvent.sdkLanguage = this._context.attribution.playwright.options.sdkLanguage;
this._contextCreatedEvent.sdkLanguage = this._sdkLanguage();
// TODO: passing the same name for two contexts makes them write into a single file
// and conflict.

View File

@ -169,7 +169,6 @@ export async function openTraceViewerApp(url: string, browserName: string, optio
const traceViewerBrowser = isUnderTest() ? 'chromium' : browserName;
const { context, page } = await launchApp(traceViewerPlaywright[traceViewerBrowser as 'chromium'], {
// TODO: store language in the trace.
sdkLanguage: traceViewerPlaywright.options.sdkLanguage,
windowSize: { width: 1280, height: 800 },
persistentContextOptions: {

View File

@ -158,6 +158,7 @@ export type LaunchOptions = channels.BrowserTypeLaunchParams & {
cdpPort?: number,
proxyOverride?: ProxySettings,
assistantMode?: boolean,
socksProxyPort?: number,
};
export type BrowserContextOptions = channels.BrowserNewContextOptions & {