chore: add Playwright to attribution (#23447)

This makes it easier to plumb all kinds of options around.
This commit is contained in:
Dmitry Gozman 2023-06-01 17:54:43 -07:00 committed by GitHub
parent 2719f408b2
commit 14a1eaa474
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 96 additions and 104 deletions

View file

@ -24,7 +24,7 @@ import { PlaywrightServer } from './remote/playwrightServer';
export class AndroidServerLauncherImpl { export class AndroidServerLauncherImpl {
async launchServer(options: LaunchAndroidServerOptions = {}): Promise<BrowserServer> { async launchServer(options: LaunchAndroidServerOptions = {}): Promise<BrowserServer> {
const playwright = createPlaywright('javascript'); const playwright = createPlaywright({ sdkLanguage: 'javascript', isServer: true });
// 1. Pre-connect to the device // 1. Pre-connect to the device
let devices = await playwright.android.devices({ let devices = await playwright.android.devices({
host: options.adbHost, host: options.adbHost,

View file

@ -36,7 +36,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
} }
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> { async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
const playwright = createPlaywright('javascript'); const playwright = createPlaywright({ sdkLanguage: 'javascript', isServer: true });
// TODO: enable socks proxy once ipv6 is supported. // TODO: enable socks proxy once ipv6 is supported.
const socksProxy = false ? new SocksProxy() : undefined; const socksProxy = false ? new SocksProxy() : undefined;
playwright.options.socksProxyPort = await socksProxy?.listen(0); playwright.options.socksProxyPort = await socksProxy?.listen(0);

View file

@ -33,7 +33,7 @@ export function printApiJson() {
export function runDriver() { export function runDriver() {
const dispatcherConnection = new DispatcherConnection(); const dispatcherConnection = new DispatcherConnection();
new RootDispatcher(dispatcherConnection, async (rootScope, { sdkLanguage }) => { new RootDispatcher(dispatcherConnection, async (rootScope, { sdkLanguage }) => {
const playwright = createPlaywright(sdkLanguage); const playwright = createPlaywright({ sdkLanguage });
return new PlaywrightDispatcher(rootScope, playwright); return new PlaywrightDispatcher(rootScope, playwright);
}); });
const transport = new PipeTransport(process.stdout, process.stdin); const transport = new PipeTransport(process.stdout, process.stdin);

View file

@ -22,7 +22,7 @@ import { AndroidServerLauncherImpl } from './androidServerImpl';
import type { Language } from './utils/isomorphic/locatorGenerators'; import type { Language } from './utils/isomorphic/locatorGenerators';
export function createInProcessPlaywright(): PlaywrightAPI { export function createInProcessPlaywright(): PlaywrightAPI {
const playwright = createPlaywright((process.env.PW_LANG_NAME as Language | undefined) || 'javascript'); const playwright = createPlaywright({ sdkLanguage: (process.env.PW_LANG_NAME as Language | undefined) || 'javascript' });
const clientConnection = new Connection(); const clientConnection = new Connection();
const dispatcherConnection = new DispatcherConnection(true /* local */); const dispatcherConnection = new DispatcherConnection(true /* local */);

View file

@ -108,7 +108,7 @@ export class PlaywrightConnection {
private async _initLaunchBrowserMode(scope: RootDispatcher) { private async _initLaunchBrowserMode(scope: RootDispatcher) {
debugLogger.log('server', `[${this._id}] engaged launch mode for "${this._options.browserName}"`); debugLogger.log('server', `[${this._id}] engaged launch mode for "${this._options.browserName}"`);
const playwright = createPlaywright('javascript'); const playwright = createPlaywright({ sdkLanguage: 'javascript', isServer: true });
const ownedSocksProxy = await this._createOwnedSocksProxy(playwright); const ownedSocksProxy = await this._createOwnedSocksProxy(playwright);
const browser = await playwright[this._options.browserName as 'chromium'].launch(serverSideCallMetadata(), this._options.launchOptions); const browser = await playwright[this._options.browserName as 'chromium'].launch(serverSideCallMetadata(), this._options.launchOptions);

View file

@ -49,9 +49,9 @@ export class PlaywrightServer {
constructor(options: ServerOptions) { constructor(options: ServerOptions) {
this._options = options; this._options = options;
if (options.preLaunchedBrowser) if (options.preLaunchedBrowser)
this._preLaunchedPlaywright = options.preLaunchedBrowser.options.rootSdkObject as Playwright; this._preLaunchedPlaywright = options.preLaunchedBrowser.attribution.playwright;
if (options.preLaunchedAndroidDevice) if (options.preLaunchedAndroidDevice)
this._preLaunchedPlaywright = options.preLaunchedAndroidDevice._android._playwrightOptions.rootSdkObject as Playwright; this._preLaunchedPlaywright = options.preLaunchedAndroidDevice._android.attribution.playwright;
} }
async listen(port: number = 0): Promise<string> { async listen(port: number = 0): Promise<string> {
@ -114,7 +114,7 @@ export class PlaywrightServer {
const isExtension = this._options.mode === 'extension'; const isExtension = this._options.mode === 'extension';
if (isExtension) { if (isExtension) {
if (!this._preLaunchedPlaywright) if (!this._preLaunchedPlaywright)
this._preLaunchedPlaywright = createPlaywright('javascript'); this._preLaunchedPlaywright = createPlaywright({ sdkLanguage: 'javascript', isServer: true });
} }
let clientType: ClientType = 'launch-browser'; let clientType: ClientType = 'launch-browser';

View file

@ -23,7 +23,7 @@ import type * as stream from 'stream';
import { wsReceiver, wsSender } from '../../utilsBundle'; import { wsReceiver, wsSender } from '../../utilsBundle';
import { createGuid, makeWaitForNextTask, isUnderTest } from '../../utils'; import { createGuid, makeWaitForNextTask, isUnderTest } from '../../utils';
import { removeFolders } from '../../utils/fileUtils'; import { removeFolders } from '../../utils/fileUtils';
import type { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser'; import type { BrowserOptions, BrowserProcess } from '../browser';
import type { BrowserContext } from '../browserContext'; import type { BrowserContext } from '../browserContext';
import { validateBrowserContextOptions } from '../browserContext'; import { validateBrowserContextOptions } from '../browserContext';
import { ProgressController } from '../progress'; import { ProgressController } from '../progress';
@ -63,12 +63,10 @@ export class Android extends SdkObject {
private _backend: Backend; private _backend: Backend;
private _devices = new Map<string, AndroidDevice>(); private _devices = new Map<string, AndroidDevice>();
readonly _timeoutSettings: TimeoutSettings; readonly _timeoutSettings: TimeoutSettings;
readonly _playwrightOptions: PlaywrightOptions;
constructor(backend: Backend, playwrightOptions: PlaywrightOptions) { constructor(parent: SdkObject, backend: Backend) {
super(playwrightOptions.rootSdkObject, 'android'); super(parent, 'android');
this._backend = backend; this._backend = backend;
this._playwrightOptions = playwrightOptions;
this._timeoutSettings = new TimeoutSettings(); this._timeoutSettings = new TimeoutSettings();
} }
@ -326,7 +324,6 @@ export class AndroidDevice extends SdkObject {
cleanupArtifactsDir().catch(e => debug('pw:android')(`could not cleanup artifacts dir: ${e}`)); cleanupArtifactsDir().catch(e => debug('pw:android')(`could not cleanup artifacts dir: ${e}`));
}); });
const browserOptions: BrowserOptions = { const browserOptions: BrowserOptions = {
...this._android._playwrightOptions,
name: 'clank', name: 'clank',
isChromium: true, isChromium: true,
slowMo: 0, slowMo: 0,
@ -342,7 +339,7 @@ export class AndroidDevice extends SdkObject {
}; };
validateBrowserContextOptions(options, browserOptions); validateBrowserContextOptions(options, browserOptions);
const browser = await CRBrowser.connect(androidBrowser, browserOptions); const browser = await CRBrowser.connect(this.attribution.playwright, androidBrowser, browserOptions);
const controller = new ProgressController(serverSideCallMetadata(), this); const controller = new ProgressController(serverSideCallMetadata(), this);
const defaultContext = browser._defaultContext!; const defaultContext = browser._defaultContext!;
await controller.run(async progress => { await controller.run(async progress => {

View file

@ -25,8 +25,6 @@ import type { RecentLogsCollector } from '../common/debugLogger';
import type { CallMetadata } from './instrumentation'; import type { CallMetadata } from './instrumentation';
import { SdkObject } from './instrumentation'; import { SdkObject } from './instrumentation';
import { Artifact } from './artifact'; import { Artifact } from './artifact';
import type { Selectors } from './selectors';
import type { Language } from '../utils/isomorphic/locatorGenerators';
export interface BrowserProcess { export interface BrowserProcess {
onclose?: ((exitCode: number | null, signal: string | null) => void); onclose?: ((exitCode: number | null, signal: string | null) => void);
@ -35,14 +33,7 @@ export interface BrowserProcess {
close(): Promise<void>; close(): Promise<void>;
} }
export type PlaywrightOptions = { export type BrowserOptions = {
rootSdkObject: SdkObject;
selectors: Selectors;
socksProxyPort?: number;
sdkLanguage: Language,
};
export type BrowserOptions = PlaywrightOptions & {
name: string, name: string,
isChromium: boolean, isChromium: boolean,
channel?: string, channel?: string,
@ -74,8 +65,8 @@ export abstract class Browser extends SdkObject {
readonly _idToVideo = new Map<string, { context: BrowserContext, artifact: Artifact }>(); readonly _idToVideo = new Map<string, { context: BrowserContext, artifact: Artifact }>();
private _contextForReuse: { context: BrowserContext, hash: string } | undefined; private _contextForReuse: { context: BrowserContext, hash: string } | undefined;
constructor(options: BrowserOptions) { constructor(parent: SdkObject, options: BrowserOptions) {
super(options.rootSdkObject, 'browser'); super(parent, 'browser');
this.attribution.browser = this; this.attribution.browser = this;
this.options = options; this.options = options;
this.instrumentation.onBrowserOpen(this); this.instrumentation.onBrowserOpen(this);

View file

@ -107,11 +107,11 @@ export abstract class BrowserContext extends SdkObject {
} }
selectors(): Selectors { selectors(): Selectors {
return this._selectors || this._browser.options.selectors; return this._selectors || this.attribution.playwright.selectors;
} }
async _initialize() { async _initialize() {
if (this.attribution.isInternalPlaywright) if (this.attribution.playwright.options.isInternalPlaywright)
return; return;
// Debugger will pause execution upon page.pause in headed mode. // Debugger will pause execution upon page.pause in headed mode.
this._debugger = new Debugger(this); this._debugger = new Debugger(this);

View file

@ -23,7 +23,7 @@ import type { BrowserName } from './registry';
import { registry } from './registry'; import { registry } from './registry';
import type { ConnectionTransport } from './transport'; import type { ConnectionTransport } from './transport';
import { WebSocketTransport } from './transport'; import { WebSocketTransport } from './transport';
import type { BrowserOptions, Browser, BrowserProcess, PlaywrightOptions } from './browser'; import type { BrowserOptions, Browser, BrowserProcess } from './browser';
import type { Env } from '../utils/processLauncher'; import type { Env } from '../utils/processLauncher';
import { launchProcess, envArrayToObject } from '../utils/processLauncher'; import { launchProcess, envArrayToObject } from '../utils/processLauncher';
import { PipeTransport } from './pipeTransport'; import { PipeTransport } from './pipeTransport';
@ -45,17 +45,15 @@ export const kNoXServerRunningError = 'Looks like you launched a headed browser
export abstract class BrowserType extends SdkObject { export abstract class BrowserType extends SdkObject {
private _name: BrowserName; private _name: BrowserName;
readonly _playwrightOptions: PlaywrightOptions;
constructor(browserName: BrowserName, playwrightOptions: PlaywrightOptions) { constructor(parent: SdkObject, browserName: BrowserName) {
super(playwrightOptions.rootSdkObject, 'browser-type'); super(parent, 'browser-type');
this.attribution.browserType = this; this.attribution.browserType = this;
this._playwrightOptions = playwrightOptions;
this._name = browserName; this._name = browserName;
} }
executablePath(): string { executablePath(): string {
return registry.findExecutable(this._name).executablePath(this._playwrightOptions.sdkLanguage) || ''; return registry.findExecutable(this._name).executablePath(this.attribution.playwright.options.sdkLanguage) || '';
} }
name(): string { name(): string {
@ -107,7 +105,6 @@ export abstract class BrowserType extends SdkObject {
if ((options as any).__testHookBeforeCreateBrowser) if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser(); await (options as any).__testHookBeforeCreateBrowser();
const browserOptions: BrowserOptions = { const browserOptions: BrowserOptions = {
...this._playwrightOptions,
name: this._name, name: this._name,
isChromium: this._name === 'chromium', isChromium: this._name === 'chromium',
channel: options.channel, channel: options.channel,
@ -184,8 +181,8 @@ export abstract class BrowserType extends SdkObject {
const registryExecutable = registry.findExecutable(options.channel || this._name); const registryExecutable = registry.findExecutable(options.channel || this._name);
if (!registryExecutable || registryExecutable.browserName !== this._name) if (!registryExecutable || registryExecutable.browserName !== this._name)
throw new Error(`Unsupported ${this._name} channel "${options.channel}"`); throw new Error(`Unsupported ${this._name} channel "${options.channel}"`);
executable = registryExecutable.executablePathOrDie(this._playwrightOptions.sdkLanguage); executable = registryExecutable.executablePathOrDie(this.attribution.playwright.options.sdkLanguage);
await registryExecutable.validateHostRequirements(this._playwrightOptions.sdkLanguage); await registryExecutable.validateHostRequirements(this.attribution.playwright.options.sdkLanguage);
} }
const waitForWSEndpoint = (options.useWebSocket || options.args?.some(a => a.startsWith('--remote-debugging-port'))) ? new ManualPromise<string>() : undefined; const waitForWSEndpoint = (options.useWebSocket || options.args?.some(a => a.startsWith('--remote-debugging-port'))) ? new ManualPromise<string>() : undefined;
@ -275,8 +272,8 @@ export abstract class BrowserType extends SdkObject {
headless = false; headless = false;
if (downloadsPath && !path.isAbsolute(downloadsPath)) if (downloadsPath && !path.isAbsolute(downloadsPath))
downloadsPath = path.join(process.cwd(), downloadsPath); downloadsPath = path.join(process.cwd(), downloadsPath);
if (this._playwrightOptions.socksProxyPort) if (this.attribution.playwright.options.socksProxyPort)
proxy = { server: `socks5://127.0.0.1:${this._playwrightOptions.socksProxyPort}` }; proxy = { server: `socks5://127.0.0.1:${this.attribution.playwright.options.socksProxyPort}` };
return { ...options, devtools, headless, downloadsPath, proxy }; return { ...options, devtools, headless, downloadsPath, proxy };
} }

View file

@ -28,7 +28,7 @@ import { BrowserType, kNoXServerRunningError } from '../browserType';
import type { ConnectionTransport, ProtocolRequest } from '../transport'; import type { ConnectionTransport, ProtocolRequest } from '../transport';
import { WebSocketTransport } from '../transport'; import { WebSocketTransport } from '../transport';
import { CRDevTools } from './crDevTools'; import { CRDevTools } from './crDevTools';
import type { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser'; import type { BrowserOptions, BrowserProcess } from '../browser';
import { Browser } from '../browser'; import { Browser } from '../browser';
import type * as types from '../types'; import type * as types from '../types';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
@ -43,7 +43,7 @@ import type { Progress } from '../progress';
import { ProgressController } from '../progress'; import { ProgressController } from '../progress';
import { TimeoutSettings } from '../../common/timeoutSettings'; import { TimeoutSettings } from '../../common/timeoutSettings';
import { helper } from '../helper'; import { helper } from '../helper';
import type { CallMetadata } from '../instrumentation'; import type { CallMetadata, SdkObject } from '../instrumentation';
import type http from 'http'; import type http from 'http';
import { registry } from '../registry'; import { registry } from '../registry';
import { ManualPromise } from '../../utils/manualPromise'; import { ManualPromise } from '../../utils/manualPromise';
@ -55,8 +55,8 @@ const ARTIFACTS_FOLDER = path.join(os.tmpdir(), 'playwright-artifacts-');
export class Chromium extends BrowserType { export class Chromium extends BrowserType {
private _devtools: CRDevTools | undefined; private _devtools: CRDevTools | undefined;
constructor(playwrightOptions: PlaywrightOptions) { constructor(parent: SdkObject) {
super('chromium', playwrightOptions); super(parent, 'chromium');
if (debugMode()) if (debugMode())
this._devtools = this._createDevTools(); this._devtools = this._createDevTools();
@ -99,7 +99,6 @@ export class Chromium extends BrowserType {
const browserProcess: BrowserProcess = { close: doClose, kill: doClose }; const browserProcess: BrowserProcess = { close: doClose, kill: doClose };
const persistent: channels.BrowserNewContextParams = { noDefaultViewport: true }; const persistent: channels.BrowserNewContextParams = { noDefaultViewport: true };
const browserOptions: BrowserOptions = { const browserOptions: BrowserOptions = {
...this._playwrightOptions,
slowMo: options.slowMo, slowMo: options.slowMo,
name: 'chromium', name: 'chromium',
isChromium: true, isChromium: true,
@ -120,7 +119,7 @@ export class Chromium extends BrowserType {
}; };
validateBrowserContextOptions(persistent, browserOptions); validateBrowserContextOptions(persistent, browserOptions);
progress.throwIfAborted(); progress.throwIfAborted();
const browser = await CRBrowser.connect(chromeTransport, browserOptions); const browser = await CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions);
browser.on(Browser.Events.Disconnected, doCleanup); browser.on(Browser.Events.Disconnected, doCleanup);
return browser; return browser;
} }
@ -137,7 +136,7 @@ export class Chromium extends BrowserType {
devtools = this._createDevTools(); devtools = this._createDevTools();
await (options as any).__testHookForDevTools(devtools); await (options as any).__testHookForDevTools(devtools);
} }
return CRBrowser.connect(transport, options, devtools); return CRBrowser.connect(this.attribution.playwright, transport, options, devtools);
} }
_rewriteStartupError(error: Error): Error { _rewriteStartupError(error: Error): Error {
@ -309,14 +308,14 @@ export class Chromium extends BrowserType {
const proxyURL = new URL(proxy.server); const proxyURL = new URL(proxy.server);
const isSocks = proxyURL.protocol === 'socks5:'; const isSocks = proxyURL.protocol === 'socks5:';
// https://www.chromium.org/developers/design-documents/network-settings // https://www.chromium.org/developers/design-documents/network-settings
if (isSocks && !this._playwrightOptions.socksProxyPort) { if (isSocks && !this.attribution.playwright.options.socksProxyPort) {
// https://www.chromium.org/developers/design-documents/network-stack/socks-proxy // https://www.chromium.org/developers/design-documents/network-stack/socks-proxy
chromeArguments.push(`--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${proxyURL.hostname}"`); chromeArguments.push(`--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${proxyURL.hostname}"`);
} }
chromeArguments.push(`--proxy-server=${proxy.server}`); chromeArguments.push(`--proxy-server=${proxy.server}`);
const proxyBypassRules = []; const proxyBypassRules = [];
// https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578 // https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578
if (this._playwrightOptions.socksProxyPort) if (this.attribution.playwright.options.socksProxyPort)
proxyBypassRules.push('<-loopback>'); proxyBypassRules.push('<-loopback>');
if (proxy.bypass) if (proxy.bypass)
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t)); proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));

View file

@ -34,6 +34,7 @@ import { readProtocolStream } from './crProtocolHelper';
import type { Protocol } from './protocol'; import type { Protocol } from './protocol';
import type { CRDevTools } from './crDevTools'; import type { CRDevTools } from './crDevTools';
import { CRServiceWorker } from './crServiceWorker'; import { CRServiceWorker } from './crServiceWorker';
import type { SdkObject } from '../instrumentation';
export class CRBrowser extends Browser { export class CRBrowser extends Browser {
readonly _connection: CRConnection; readonly _connection: CRConnection;
@ -51,11 +52,11 @@ export class CRBrowser extends Browser {
private _tracingClient: CRSession | undefined; private _tracingClient: CRSession | undefined;
private _userAgent: string = ''; private _userAgent: string = '';
static async connect(transport: ConnectionTransport, options: BrowserOptions, devtools?: CRDevTools): Promise<CRBrowser> { static async connect(parent: SdkObject, transport: ConnectionTransport, options: BrowserOptions, devtools?: CRDevTools): Promise<CRBrowser> {
// Make a copy in case we need to update `headful` property below. // Make a copy in case we need to update `headful` property below.
options = { ...options }; options = { ...options };
const connection = new CRConnection(transport, options.protocolLogger, options.browserLogsCollector); const connection = new CRConnection(transport, options.protocolLogger, options.browserLogsCollector);
const browser = new CRBrowser(connection, options); const browser = new CRBrowser(parent, connection, options);
browser._devtools = devtools; browser._devtools = devtools;
const session = connection.rootSession; const session = connection.rootSession;
if ((options as any).__testHookOnConnectToBrowser) if ((options as any).__testHookOnConnectToBrowser)
@ -85,8 +86,8 @@ export class CRBrowser extends Browser {
return browser; return browser;
} }
constructor(connection: CRConnection, options: BrowserOptions) { constructor(parent: SdkObject, connection: CRConnection, options: BrowserOptions) {
super(options); super(parent, options);
this._connection = connection; this._connection = connection;
this._session = this._connection.rootSession; this._session = this._connection.rootSession;
this._connection.on(ConnectionEvents.Disconnected, () => this._didClose()); this._connection.on(ConnectionEvents.Disconnected, () => this._didClose());

View file

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

View file

@ -86,7 +86,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
const selectorsRegistry = this.frame._page.context().selectors(); const selectorsRegistry = this.frame._page.context().selectors();
for (const [name, { source }] of selectorsRegistry._engines) for (const [name, { source }] of selectorsRegistry._engines)
custom.push(`{ name: '${name}', engine: (${source}) }`); custom.push(`{ name: '${name}', engine: (${source}) }`);
const sdkLanguage = this.frame._page.context()._browser.options.sdkLanguage; const sdkLanguage = this.frame.attribution.playwright.options.sdkLanguage;
const source = ` const source = `
(() => { (() => {
const module = {}; const module = {};

View file

@ -35,7 +35,8 @@ import type { Progress } from '../progress';
import { ProgressController } from '../progress'; import { ProgressController } from '../progress';
import { helper } from '../helper'; import { helper } from '../helper';
import { eventsHelper } from '../../utils/eventsHelper'; import { eventsHelper } from '../../utils/eventsHelper';
import type { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser'; import type { BrowserOptions, BrowserProcess } from '../browser';
import type { Playwright } from '../playwright';
import type * as childProcess from 'child_process'; import type * as childProcess from 'child_process';
import * as readline from 'readline'; import * as readline from 'readline';
import { RecentLogsCollector } from '../../common/debugLogger'; import { RecentLogsCollector } from '../../common/debugLogger';
@ -116,11 +117,8 @@ export class ElectronApplication extends SdkObject {
} }
export class Electron extends SdkObject { export class Electron extends SdkObject {
private _playwrightOptions: PlaywrightOptions; constructor(playwright: Playwright) {
super(playwright, 'electron');
constructor(playwrightOptions: PlaywrightOptions) {
super(playwrightOptions.rootSdkObject, 'electron');
this._playwrightOptions = playwrightOptions;
} }
async launch(options: channels.ElectronLaunchParams): Promise<ElectronApplication> { async launch(options: channels.ElectronLaunchParams): Promise<ElectronApplication> {
@ -224,7 +222,6 @@ export class Electron extends SdkObject {
noDefaultViewport: true, noDefaultViewport: true,
}; };
const browserOptions: BrowserOptions = { const browserOptions: BrowserOptions = {
...this._playwrightOptions,
name: 'electron', name: 'electron',
isChromium: true, isChromium: true,
headful: true, headful: true,
@ -238,7 +235,7 @@ export class Electron extends SdkObject {
originalLaunchOptions: {}, originalLaunchOptions: {},
}; };
validateBrowserContextOptions(contextOptions, browserOptions); validateBrowserContextOptions(contextOptions, browserOptions);
const browser = await CRBrowser.connect(chromeTransport, browserOptions); const browser = await CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions);
app = new ElectronApplication(this, browser, nodeConnection, launchedProcess); app = new ElectronApplication(this, browser, nodeConnection, launchedProcess);
await app.initialize(); await app.initialize();
return app; return app;

View file

@ -28,6 +28,7 @@ import type * as channels from '@protocol/channels';
import { ConnectionEvents, FFConnection } from './ffConnection'; import { ConnectionEvents, FFConnection } from './ffConnection';
import { FFPage } from './ffPage'; import { FFPage } from './ffPage';
import type { Protocol } from './protocol'; import type { Protocol } from './protocol';
import type { SdkObject } from '../instrumentation';
export class FFBrowser extends Browser { export class FFBrowser extends Browser {
_connection: FFConnection; _connection: FFConnection;
@ -36,9 +37,9 @@ export class FFBrowser extends Browser {
private _version = ''; private _version = '';
private _userAgent: string = ''; private _userAgent: string = '';
static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> { static async connect(parent: SdkObject, transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> {
const connection = new FFConnection(transport, options.protocolLogger, options.browserLogsCollector); const connection = new FFConnection(transport, options.protocolLogger, options.browserLogsCollector);
const browser = new FFBrowser(connection, options); const browser = new FFBrowser(parent, connection, options);
if ((options as any).__testHookOnConnectToBrowser) if ((options as any).__testHookOnConnectToBrowser)
await (options as any).__testHookOnConnectToBrowser(); await (options as any).__testHookOnConnectToBrowser();
let firefoxUserPrefs = options.persistent ? {} : options.originalLaunchOptions.firefoxUserPrefs ?? {}; let firefoxUserPrefs = options.persistent ? {} : options.originalLaunchOptions.firefoxUserPrefs ?? {};
@ -61,8 +62,8 @@ export class FFBrowser extends Browser {
return browser; return browser;
} }
constructor(connection: FFConnection, options: BrowserOptions) { constructor(parent: SdkObject, connection: FFConnection, options: BrowserOptions) {
super(options); super(parent, options);
this._connection = connection; this._connection = connection;
this._ffPages = new Map(); this._ffPages = new Map();
this._contexts = new Map(); this._contexts = new Map();

View file

@ -22,18 +22,19 @@ import { kBrowserCloseMessageId } from './ffConnection';
import { BrowserType, kNoXServerRunningError } from '../browserType'; import { BrowserType, kNoXServerRunningError } from '../browserType';
import type { Env } from '../../utils/processLauncher'; import type { Env } from '../../utils/processLauncher';
import type { ConnectionTransport } from '../transport'; import type { ConnectionTransport } from '../transport';
import type { BrowserOptions, PlaywrightOptions } from '../browser'; import type { BrowserOptions } from '../browser';
import type * as types from '../types'; import type * as types from '../types';
import { rewriteErrorMessage } from '../../utils/stackTrace'; import { rewriteErrorMessage } from '../../utils/stackTrace';
import { wrapInASCIIBox } from '../../utils'; import { wrapInASCIIBox } from '../../utils';
import type { SdkObject } from '../instrumentation';
export class Firefox extends BrowserType { export class Firefox extends BrowserType {
constructor(playwrightOptions: PlaywrightOptions) { constructor(parent: SdkObject) {
super('firefox', playwrightOptions); super(parent, 'firefox');
} }
_connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> { _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> {
return FFBrowser.connect(transport, options); return FFBrowser.connect(this.attribution.playwright, transport, options);
} }
_rewriteStartupError(error: Error): Error { _rewriteStartupError(error: Error): Error {

View file

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

View file

@ -23,9 +23,10 @@ import type { BrowserType } from './browserType';
import type { ElementHandle } from './dom'; import type { ElementHandle } from './dom';
import type { Frame } from './frames'; import type { Frame } from './frames';
import type { Page } from './page'; import type { Page } from './page';
import type { Playwright } from './playwright';
export type Attribution = { export type Attribution = {
isInternalPlaywright: boolean, playwright: Playwright;
browserType?: BrowserType; browserType?: BrowserType;
browser?: Browser; browser?: Browser;
context?: BrowserContext | APIRequestContext; context?: BrowserContext | APIRequestContext;

View file

@ -16,7 +16,7 @@
import { Android } from './android/android'; import { Android } from './android/android';
import { AdbBackend } from './android/backendAdb'; import { AdbBackend } from './android/backendAdb';
import type { Browser, PlaywrightOptions } from './browser'; import type { Browser } from './browser';
import { Chromium } from './chromium/chromium'; import { Chromium } from './chromium/chromium';
import { Electron } from './electron/electron'; import { Electron } from './electron/electron';
import { Firefox } from './firefox/firefox'; import { Firefox } from './firefox/firefox';
@ -29,6 +29,13 @@ import type { Page } from './page';
import { DebugController } from './debugController'; import { DebugController } from './debugController';
import type { Language } from '../utils/isomorphic/locatorGenerators'; import type { Language } from '../utils/isomorphic/locatorGenerators';
type PlaywrightOptions = {
socksProxyPort?: number;
sdkLanguage: Language;
isInternalPlaywright?: boolean;
isServer?: boolean;
};
export class Playwright extends SdkObject { export class Playwright extends SdkObject {
readonly selectors: Selectors; readonly selectors: Selectors;
readonly chromium: Chromium; readonly chromium: Chromium;
@ -41,8 +48,10 @@ export class Playwright extends SdkObject {
private _allPages = new Set<Page>(); private _allPages = new Set<Page>();
private _allBrowsers = new Set<Browser>(); private _allBrowsers = new Set<Browser>();
constructor(sdkLanguage: Language, isInternalPlaywright: boolean) { constructor(options: PlaywrightOptions) {
super({ attribution: { isInternalPlaywright }, instrumentation: createInstrumentation() } as any, undefined, 'Playwright'); super({ attribution: {}, instrumentation: createInstrumentation() } as any, undefined, 'Playwright');
this.options = options;
this.attribution.playwright = this;
this.instrumentation.addListener({ this.instrumentation.addListener({
onBrowserOpen: browser => this._allBrowsers.add(browser), onBrowserOpen: browser => this._allBrowsers.add(browser),
onBrowserClose: browser => this._allBrowsers.delete(browser), onBrowserClose: browser => this._allBrowsers.delete(browser),
@ -52,17 +61,12 @@ export class Playwright extends SdkObject {
debugLogger.log(logName as any, message); debugLogger.log(logName as any, message);
} }
}, null); }, null);
this.options = { this.chromium = new Chromium(this);
rootSdkObject: this, this.firefox = new Firefox(this);
selectors: new Selectors(), this.webkit = new WebKit(this);
sdkLanguage: sdkLanguage, this.electron = new Electron(this);
}; this.android = new Android(this, new AdbBackend());
this.chromium = new Chromium(this.options); this.selectors = new Selectors();
this.firefox = new Firefox(this.options);
this.webkit = new WebKit(this.options);
this.electron = new Electron(this.options);
this.android = new Android(new AdbBackend(), this.options);
this.selectors = this.options.selectors;
this.debugController = new DebugController(this); this.debugController = new DebugController(this);
} }
@ -79,6 +83,6 @@ export class Playwright extends SdkObject {
} }
} }
export function createPlaywright(sdkLanguage: Language, isInternalPlaywright: boolean = false) { export function createPlaywright(options: PlaywrightOptions) {
return new Playwright(sdkLanguage, isInternalPlaywright); return new Playwright(options);
} }

View file

@ -358,7 +358,7 @@ class ContextRecorder extends EventEmitter {
this._context = context; this._context = context;
this._params = params; this._params = params;
this._recorderSources = []; this._recorderSources = [];
const language = params.language || context._browser.options.sdkLanguage; const language = params.language || context.attribution.playwright.options.sdkLanguage;
this.setOutput(language, params.outputFile); this.setOutput(language, params.outputFile);
const generator = new CodeGenerator(context._browser.options.name, params.mode === 'recording', params.launchOptions || {}, params.contextOptions || {}, params.device, params.saveStorage); const generator = new CodeGenerator(context._browser.options.name, params.mode === 'recording', params.launchOptions || {}, params.contextOptions || {}, params.device, params.saveStorage);
generator.on('change', () => { generator.on('change', () => {

View file

@ -114,9 +114,9 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
} }
static async open(recorder: Recorder, inspectedContext: BrowserContext, handleSIGINT: boolean | undefined): Promise<IRecorderApp> { static async open(recorder: Recorder, inspectedContext: BrowserContext, handleSIGINT: boolean | undefined): Promise<IRecorderApp> {
const sdkLanguage = inspectedContext._browser.options.sdkLanguage; const sdkLanguage = inspectedContext.attribution.playwright.options.sdkLanguage;
const headed = !!inspectedContext._browser.options.headful; const headed = !!inspectedContext._browser.options.headful;
const recorderPlaywright = (require('../playwright').createPlaywright as typeof import('../playwright').createPlaywright)('javascript', true); const recorderPlaywright = (require('../playwright').createPlaywright as typeof import('../playwright').createPlaywright)({ sdkLanguage: 'javascript', isInternalPlaywright: true });
const args = [ const args = [
'--app=data:text/html,', '--app=data:text/html,',
'--window-size=600,600', '--window-size=600,600',

View file

@ -99,7 +99,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
options: {}, options: {},
platform: process.platform, platform: process.platform,
wallTime: 0, wallTime: 0,
sdkLanguage: (context as BrowserContext)?._browser?.options?.sdkLanguage, sdkLanguage: context.attribution.playwright.options.sdkLanguage,
testIdAttributeName testIdAttributeName
}; };
if (context instanceof BrowserContext) { if (context instanceof BrowserContext) {
@ -119,7 +119,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
throw new Error('Cannot start tracing while stopping'); throw new Error('Cannot start tracing while stopping');
// Re-write for testing. // Re-write for testing.
this._contextCreatedEvent.sdkLanguage = (this._context as BrowserContext)?._browser?.options?.sdkLanguage; this._contextCreatedEvent.sdkLanguage = this._context.attribution.playwright.options.sdkLanguage;
if (this._state) { if (this._state) {
const o = this._state.options; const o = this._state.options;

View file

@ -74,7 +74,7 @@ export async function showTraceViewer(traceUrls: string[], browserName: string,
const urlPrefix = await server.start({ preferredPort: port, host }); const urlPrefix = await server.start({ preferredPort: port, host });
const traceViewerPlaywright = createPlaywright('javascript', true); const traceViewerPlaywright = createPlaywright({ sdkLanguage: 'javascript', isInternalPlaywright: true });
const traceViewerBrowser = isUnderTest() ? 'chromium' : browserName; const traceViewerBrowser = isUnderTest() ? 'chromium' : browserName;
const args = traceViewerBrowser === 'chromium' ? [ const args = traceViewerBrowser === 'chromium' ? [
'--app=data:text/html,', '--app=data:text/html,',

View file

@ -21,18 +21,19 @@ import path from 'path';
import { kBrowserCloseMessageId } from './wkConnection'; import { kBrowserCloseMessageId } from './wkConnection';
import { BrowserType, kNoXServerRunningError } from '../browserType'; import { BrowserType, kNoXServerRunningError } from '../browserType';
import type { ConnectionTransport } from '../transport'; import type { ConnectionTransport } from '../transport';
import type { BrowserOptions, PlaywrightOptions } from '../browser'; import type { BrowserOptions } from '../browser';
import type * as types from '../types'; import type * as types from '../types';
import { rewriteErrorMessage } from '../../utils/stackTrace'; import { rewriteErrorMessage } from '../../utils/stackTrace';
import { wrapInASCIIBox } from '../../utils'; import { wrapInASCIIBox } from '../../utils';
import type { SdkObject } from '../instrumentation';
export class WebKit extends BrowserType { export class WebKit extends BrowserType {
constructor(playwrightOptions: PlaywrightOptions) { constructor(parent: SdkObject) {
super('webkit', playwrightOptions); super(parent, 'webkit');
} }
_connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<WKBrowser> { _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<WKBrowser> {
return WKBrowser.connect(transport, options); return WKBrowser.connect(this.attribution.playwright, transport, options);
} }
_amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env { _amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {

View file

@ -31,6 +31,7 @@ import type { PageProxyMessageReceivedPayload } from './wkConnection';
import { kPageProxyMessageReceived, WKConnection, WKSession } from './wkConnection'; import { kPageProxyMessageReceived, WKConnection, WKSession } from './wkConnection';
import { WKPage } from './wkPage'; import { WKPage } from './wkPage';
import { kBrowserClosedError } from '../../common/errors'; import { kBrowserClosedError } from '../../common/errors';
import type { SdkObject } from '../instrumentation';
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15'; const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15';
const BROWSER_VERSION = '16.4'; const BROWSER_VERSION = '16.4';
@ -42,8 +43,8 @@ export class WKBrowser extends Browser {
readonly _wkPages = new Map<string, WKPage>(); readonly _wkPages = new Map<string, WKPage>();
private readonly _eventListeners: RegisteredListener[]; private readonly _eventListeners: RegisteredListener[];
static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<WKBrowser> { static async connect(parent: SdkObject, transport: ConnectionTransport, options: BrowserOptions): Promise<WKBrowser> {
const browser = new WKBrowser(transport, options); const browser = new WKBrowser(parent, transport, options);
if ((options as any).__testHookOnConnectToBrowser) if ((options as any).__testHookOnConnectToBrowser)
await (options as any).__testHookOnConnectToBrowser(); await (options as any).__testHookOnConnectToBrowser();
const promises: Promise<any>[] = [ const promises: Promise<any>[] = [
@ -58,8 +59,8 @@ export class WKBrowser extends Browser {
return browser; return browser;
} }
constructor(transport: ConnectionTransport, options: BrowserOptions) { constructor(parent: SdkObject, transport: ConnectionTransport, options: BrowserOptions) {
super(options); super(parent, options);
this._connection = new WKConnection(transport, this._onDisconnect.bind(this), options.protocolLogger, options.browserLogsCollector); this._connection = new WKConnection(transport, this._onDisconnect.bind(this), options.protocolLogger, options.browserLogsCollector);
this._browserSession = this._connection.browserSession; this._browserSession = this._connection.browserSession;
this._eventListeners = [ this._eventListeners = [

View file

@ -774,7 +774,7 @@ test('should display waitForLoadState even if did not wait for it', async ({ run
}); });
test('should display language-specific locators', async ({ runAndTrace, server, page, toImpl }) => { test('should display language-specific locators', async ({ runAndTrace, server, page, toImpl }) => {
toImpl(page.context())._browser.options.sdkLanguage = 'python'; toImpl(page).attribution.playwright.options.sdkLanguage = 'python';
const traceViewer = await runAndTrace(async () => { const traceViewer = await runAndTrace(async () => {
await page.setContent('<button>Submit</button>'); await page.setContent('<button>Submit</button>');
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
@ -783,6 +783,7 @@ test('should display language-specific locators', async ({ runAndTrace, server,
/page.setContent/, /page.setContent/,
/locator.clickget_by_role\("button", name="Submit"\)/, /locator.clickget_by_role\("button", name="Submit"\)/,
]); ]);
toImpl(page).attribution.playwright.options.sdkLanguage = 'javascript';
}); });
test('should pick locator', async ({ page, runAndTrace, server }) => { test('should pick locator', async ({ page, runAndTrace, server }) => {