chore: fix selector engines in electron and android (#36119)

This commit is contained in:
Dmitry Gozman 2025-05-29 11:37:15 +00:00 committed by GitHub
parent 8f773a4c06
commit 111f21ebac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 53 additions and 29 deletions

View File

@ -32,11 +32,13 @@ import type * as api from '../../types/types';
import type { AndroidServerLauncherImpl } from '../androidServerImpl';
import type { Platform } from './platform';
import type * as channels from '@protocol/channels';
import type { Playwright } from './playwright';
type Direction = 'down' | 'up' | 'left' | 'right';
type SpeedOptions = { speed?: number };
export class Android extends ChannelOwner<channels.AndroidChannel> implements api.Android {
_playwright!: Playwright;
readonly _timeoutSettings: TimeoutSettings;
_serverLauncher?: AndroidServerLauncherImpl;
@ -100,6 +102,7 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> implements api.AndroidDevice {
readonly _timeoutSettings: TimeoutSettings;
private _webViews = new Map<string, AndroidWebView>();
private _android: Android;
_shouldCloseConnectionOnClose = false;
static from(androidDevice: channels.AndroidDeviceChannel): AndroidDevice {
@ -110,6 +113,7 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.AndroidDeviceInitializer) {
super(parent, type, guid, initializer);
this._android = parent as Android;
this.input = new AndroidInput(this);
this._timeoutSettings = new TimeoutSettings(this._platform, (parent as Android)._timeoutSettings);
this._channel.on('webViewAdded', ({ webView }) => this._onWebViewAdded(webView));
@ -257,7 +261,10 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
async launchBrowser(options: types.BrowserContextOptions & { pkg?: string } = {}): Promise<BrowserContext> {
const contextOptions = await prepareBrowserContextParams(this._platform, options);
const result = await this._channel.launchBrowser(contextOptions);
const context = BrowserContext.from(result.context) as BrowserContext;
const context = BrowserContext.from(result.context);
const selectors = this._android._playwright.selectors;
selectors._contextsForSelectors.add(context);
context.once(Events.BrowserContext.Close, () => selectors._contextsForSelectors.delete(context));
await context._initializeHarFromOptions(options.recordHar);
return context;
}

View File

@ -76,12 +76,10 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
}
async _innerNewContext(options: BrowserContextOptions = {}, forReuse: boolean): Promise<BrowserContext> {
options = {
options = this._browserType._playwright.selectors._withSelectorOptions({
...this._browserType._playwright._defaultContextOptions,
...options,
selectorEngines: this._browserType._playwright.selectors._selectorEngines,
testIdAttributeName: this._browserType._playwright.selectors._testIdAttributeName,
};
});
const contextOptions = await prepareBrowserContextParams(this._platform, options);
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
const context = BrowserContext.from(response.context);
@ -117,6 +115,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
context._logger = this._logger;
context.tracing._tracesDir = this._options.tracesDir;
this._browserType._contexts.add(context);
this._browserType._playwright.selectors._contextsForSelectors.add(context);
context.setDefaultTimeout(this._browserType._playwright._defaultContextTimeout);
context.setDefaultNavigationTimeout(this._browserType._playwright._defaultContextNavigationTimeout);
}

View File

@ -458,6 +458,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
this._closingStatus = 'closed';
this._browser?._contexts.delete(this);
this._browser?._browserType._contexts.delete(this);
this._browser?._browserType._playwright.selectors._contextsForSelectors.delete(this);
this._disposeHarRouters();
this.tracing._resetStackCounter();
this.emit(Events.BrowserContext.Close, this);

View File

@ -93,13 +93,11 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
async launchPersistentContext(userDataDir: string, options: LaunchPersistentContextOptions = {}): Promise<BrowserContext> {
const logger = options.logger || this._playwright._defaultLaunchOptions?.logger;
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
options = {
options = this._playwright.selectors._withSelectorOptions({
...this._playwright._defaultLaunchOptions,
...this._playwright._defaultContextOptions,
...options,
selectorEngines: this._playwright.selectors._selectorEngines,
testIdAttributeName: this._playwright.selectors._testIdAttributeName,
};
});
const contextParams = await prepareBrowserContextParams(this._platform, options);
const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = {
...contextParams,
@ -166,8 +164,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
connection.close();
throw new Error('Malformed endpoint. Did you use BrowserType.launchServer method?');
}
this._playwright.selectors._playwrights.add(playwright);
connection.on('close', () => this._playwright.selectors._playwrights.delete(playwright));
playwright.selectors = this._playwright.selectors;
browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
browser._connectToBrowserType(this, {}, logger);
browser._shouldCloseConnectionOnClose = true;

View File

@ -31,6 +31,7 @@ import type * as api from '../../types/types';
import type * as channels from '@protocol/channels';
import type * as childProcess from 'child_process';
import type { BrowserWindow } from 'electron';
import type { Playwright } from './playwright';
type ElectronOptions = Omit<channels.ElectronLaunchOptions, 'env'|'extraHTTPHeaders'|'recordHar'|'colorScheme'|'acceptDownloads'> & {
env?: Env,
@ -44,6 +45,8 @@ type ElectronOptions = Omit<channels.ElectronLaunchOptions, 'env'|'extraHTTPHead
type ElectronAppType = typeof import('electron');
export class Electron extends ChannelOwner<channels.ElectronChannel> implements api.Electron {
_playwright!: Playwright;
static from(electron: channels.ElectronChannel): Electron {
return (electron as any)._object;
}
@ -53,6 +56,7 @@ export class Electron extends ChannelOwner<channels.ElectronChannel> implements
}
async launch(options: ElectronOptions = {}): Promise<ElectronApplication> {
options = this._playwright.selectors._withSelectorOptions(options);
const params: channels.ElectronLaunchParams = {
...await prepareBrowserContextParams(this._platform, options),
env: envObjectToArray(options.env ? options.env : this._platform.env),
@ -60,6 +64,8 @@ export class Electron extends ChannelOwner<channels.ElectronChannel> implements
timeout: new TimeoutSettings(this._platform).launchTimeout(options),
};
const app = ElectronApplication.from((await this._channel.launch(params)).electronApplication);
this._playwright.selectors._contextsForSelectors.add(app._context);
app.once(Events.ElectronApplication.Close, () => this._playwright.selectors._contextsForSelectors.delete(app._context));
await app._context._initializeHarFromOptions(options.recordHar);
app._context.tracing._tracesDir = options.tracesDir;
return app;

View File

@ -55,14 +55,15 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
this.webkit = BrowserType.from(initializer.webkit);
this.webkit._playwright = this;
this._android = Android.from(initializer.android);
this._android._playwright = this;
this._electron = Electron.from(initializer.electron);
this._electron._playwright = this;
this._bidiChromium = BrowserType.from(initializer.bidiChromium);
this._bidiChromium._playwright = this;
this._bidiFirefox = BrowserType.from(initializer.bidiFirefox);
this._bidiFirefox._playwright = this;
this.devices = this._connection.localUtils()?.devices ?? {};
this.selectors = new Selectors();
this.selectors._playwrights.add(this);
this.selectors = new Selectors(this._connection._platform);
this.errors = { TimeoutError };
(global as any)._playwrightInstance = this;
}

View File

@ -20,30 +20,35 @@ import { setTestIdAttribute } from './locator';
import type { SelectorEngine } from './types';
import type * as api from '../../types/types';
import type * as channels from '@protocol/channels';
import type { Playwright } from './playwright';
import type { BrowserContext } from './browserContext';
import type { Platform } from './platform';
export class Selectors implements api.Selectors {
_playwrights = new Set<Playwright>();
_selectorEngines: channels.SelectorEngine[] = [];
_testIdAttributeName: string | undefined;
private _platform: Platform;
private _selectorEngines: channels.SelectorEngine[] = [];
private _testIdAttributeName: string | undefined;
readonly _contextsForSelectors = new Set<BrowserContext>();
constructor(platform: Platform) {
this._platform = platform;
}
async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise<void> {
const platform = this._playwrights.values().next().value!._platform;
const source = await evaluationScript(platform, script, undefined, false);
const source = await evaluationScript(this._platform, script, undefined, false);
const selectorEngine: channels.SelectorEngine = { ...options, name, source };
for (const playwright of this._playwrights) {
for (const context of playwright._allContexts())
await context._channel.registerSelectorEngine({ selectorEngine });
}
for (const context of this._contextsForSelectors)
await context._channel.registerSelectorEngine({ selectorEngine });
this._selectorEngines.push(selectorEngine);
}
setTestIdAttribute(attributeName: string) {
this._testIdAttributeName = attributeName;
setTestIdAttribute(attributeName);
for (const playwright of this._playwrights) {
for (const context of playwright._allContexts())
context._channel.setTestIdAttributeName({ testIdAttributeName: attributeName }).catch(() => {});
}
for (const context of this._contextsForSelectors)
context._channel.setTestIdAttributeName({ testIdAttributeName: attributeName }).catch(() => {});
}
_withSelectorOptions<T>(options: T) {
return { ...options, selectorEngines: this._selectorEngines, testIdAttributeName: this._testIdAttributeName };
}
}

View File

@ -2528,6 +2528,8 @@ scheme.ElectronLaunchParams = tObject({
strictSelectors: tOptional(tBoolean),
timezoneId: tOptional(tString),
tracesDir: tOptional(tString),
selectorEngines: tOptional(tArray(tType('SelectorEngine'))),
testIdAttributeName: tOptional(tString),
});
scheme.ElectronLaunchResult = tObject({
electronApplication: tChannel(['ElectronApplication']),

View File

@ -4397,6 +4397,8 @@ export type ElectronLaunchParams = {
strictSelectors?: boolean,
timezoneId?: string,
tracesDir?: string,
selectorEngines?: SelectorEngine[],
testIdAttributeName?: string,
};
export type ElectronLaunchOptions = {
executablePath?: string,
@ -4430,6 +4432,8 @@ export type ElectronLaunchOptions = {
strictSelectors?: boolean,
timezoneId?: string,
tracesDir?: string,
selectorEngines?: SelectorEngine[],
testIdAttributeName?: string,
};
export type ElectronLaunchResult = {
electronApplication: ElectronApplicationChannel,

View File

@ -3745,6 +3745,10 @@ Electron:
strictSelectors: boolean?
timezoneId: string?
tracesDir: string?
selectorEngines:
type: array?
items: SelectorEngine
testIdAttributeName: string?
returns:
electronApplication: ElectronApplication

View File

@ -17,7 +17,6 @@
import { test } from '@playwright/test';
import type { TestModeName } from './testMode';
import { DefaultTestMode, DriverTestMode } from './testMode';
import * as playwrightLibrary from 'playwright-core';
export type TestModeWorkerOptions = {
mode: TestModeName;
@ -43,7 +42,6 @@ export const testModeTest = test.extend<TestModeTestFixtures, TestModeWorkerOpti
'driver': new DriverTestMode(),
}[mode];
const playwright = await testMode.setup();
(playwrightLibrary.selectors as any)._playwrights.add(playwright);
await run(playwright);
await testMode.teardown();
}, { scope: 'worker' }],