chore: cleanup runtimeGuid and other builtins bits (#36035)

This commit is contained in:
Dmitry Gozman 2025-05-21 21:10:41 +00:00 committed by GitHub
parent 28e925c001
commit fd7d249ed4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 50 additions and 97 deletions

View File

@ -18,13 +18,6 @@ import { serializeAsCallArgument } from '@isomorphic/utilityScriptSerializers';
import type { SerializedValue } from '@isomorphic/utilityScriptSerializers';
// This runtime guid is replaced by the actual guid at runtime in all generated sources.
const kRuntimeGuid = '$runtime_guid$';
// The name of the global playwright binding, referenced in Node.js.
const kPlaywrightBinding = `__playwright__binding__${kRuntimeGuid}`;
const kPlaywrightBindingController = `__playwright__binding__controller__${kRuntimeGuid}`;
export type BindingPayload = {
name: string;
seq: number;
@ -37,14 +30,16 @@ type BindingData = {
handles: Map<number, any>;
};
class BindingsController {
export class BindingsController {
// eslint-disable-next-line no-restricted-globals
private _global: typeof globalThis;
private _globalBindingName: string;
private _bindings = new Map<string, BindingData>();
// eslint-disable-next-line no-restricted-globals
constructor(global: typeof globalThis) {
constructor(global: typeof globalThis, globalBindingName: string) {
this._global = global;
this._globalBindingName = globalBindingName;
}
addBinding(bindingName: string, needsHandle: boolean) {
@ -72,7 +67,7 @@ class BindingsController {
}
payload = { name: bindingName, seq, serializedArgs };
}
(this._global as any)[kPlaywrightBinding](JSON.stringify(payload));
(this._global as any)[this._globalBindingName](JSON.stringify(payload));
return promise;
};
}
@ -93,10 +88,3 @@ class BindingsController {
callbacks.delete(arg.seq);
}
}
export function ensureBindingsController() {
// eslint-disable-next-line no-restricted-globals
const global = globalThis;
if (!(global as any)[kPlaywrightBindingController])
(global as any)[kPlaywrightBindingController] = new BindingsController(global);
}

View File

@ -66,7 +66,7 @@ interface WebKitLegacyDeviceMotionEvent extends DeviceMotionEvent {
}
export type InjectedScriptOptions = {
isUnderTest?: boolean;
isUnderTest: boolean;
sdkLanguage: Language;
// For strict error and codegen
testIdAttributeName: string;
@ -124,11 +124,10 @@ export class InjectedScript {
constructor(window: Window & typeof globalThis, options: InjectedScriptOptions) {
this.window = window;
this.document = window.document;
this.isUnderTest = options.isUnderTest;
// Make sure builtins are created from "window". This is important for InjectedScript instantiated
// inside a trace viewer snapshot, where "window" differs from "globalThis".
const utilityScript = new UtilityScript(window);
this.isUnderTest = options.isUnderTest ?? utilityScript.isUnderTest;
this.utils.builtins = utilityScript.builtins;
this.utils.builtins = new UtilityScript(window, options.isUnderTest).builtins;
this._sdkLanguage = options.sdkLanguage;
this._testIdAttributeNameForStrictErrorAndConsoleCodegen = options.testIdAttributeName;
this._evaluator = new SelectorEvaluatorImpl();

View File

@ -16,11 +16,6 @@
import { parseEvaluationResultValue, serializeAsCallArgument } from '@isomorphic/utilityScriptSerializers';
// --- This section should match javascript.ts and generated_injected_builtins.js ---
// This flag is replaced by true/false at runtime in all generated sources.
const kUtilityScriptIsUnderTest = false;
// Keep in sync with eslint.config.mjs
export type Builtins = {
setTimeout: Window['setTimeout'],
@ -38,8 +33,6 @@ export type Builtins = {
Date: typeof window['Date'],
};
// --- End of the matching section ---
export class UtilityScript {
// eslint-disable-next-line no-restricted-globals
readonly global: typeof globalThis;
@ -48,9 +41,9 @@ export class UtilityScript {
readonly isUnderTest: boolean;
// eslint-disable-next-line no-restricted-globals
constructor(global: typeof globalThis) {
constructor(global: typeof globalThis, isUnderTest: boolean) {
this.global = global;
this.isUnderTest = kUtilityScriptIsUnderTest;
this.isUnderTest = isUnderTest;
if ((global as any).__pwClock) {
this.builtins = (global as any).__pwClock.builtins;
} else {

View File

@ -21,7 +21,7 @@ import * as network from '../network';
import { BidiConnection } from './bidiConnection';
import { bidiBytesValueToString } from './bidiNetworkManager';
import { BidiPage, kPlaywrightBindingChannel } from './bidiPage';
import { kPlaywrightBinding } from '../javascript';
import { PageBinding } from '../page';
import * as bidi from './third_party/bidiProtocol';
import type { RegisteredListener } from '../utils/eventsHelper';
@ -391,7 +391,7 @@ export class BidiBrowserContext extends BrowserContext {
ownership: bidi.Script.ResultOwnership.Root,
}
}];
const functionDeclaration = `function addMainBinding(callback) { globalThis['${kPlaywrightBinding}'] = callback; }`;
const functionDeclaration = `function addMainBinding(callback) { globalThis['${PageBinding.kBindingName}'] = callback; }`;
const promises = [];
promises.push(this._browser._browserSession.send('script.addPreloadScript', {
functionDeclaration,

View File

@ -34,9 +34,7 @@ import { Recorder } from './recorder';
import { RecorderApp } from './recorder/recorderApp';
import { Selectors } from './selectors';
import { Tracing } from './trace/recorder/tracing';
import * as js from './javascript';
import * as rawStorageSource from '../generated/storageScriptSource';
import * as rawBindingsControllerSource from '../generated/bindingsControllerSource';
import type { Artifact } from './artifact';
import type { Browser, BrowserOptions } from './browser';
@ -327,13 +325,7 @@ export abstract class BrowserContext extends SdkObject {
this._playwrightBindingExposed = true;
await this.doExposePlaywrightBinding();
this.bindingsInitScript = new InitScript(`
(() => {
const module = {};
${js.prepareGeneratedScript(rawBindingsControllerSource.source)}
(module.exports.ensureBindingsController())();
})();
`, true /* internal */);
this.bindingsInitScript = PageBinding.createInitScript();
this.initScripts.push(this.bindingsInitScript);
await this.doAddInitScript(this.bindingsInitScript);
await this.safeNonStallingEvaluateInAllFrames(this.bindingsInitScript.source, 'main');
@ -530,7 +522,7 @@ export abstract class BrowserContext extends SdkObject {
const collectScript = `(() => {
const module = {};
${js.prepareGeneratedScript(rawStorageSource.source)}
${rawStorageSource.source}
const script = new (module.exports.StorageScript())(${this._browser.options.name === 'firefox'});
return script.collect(${indexedDB});
})()`;
@ -628,7 +620,7 @@ export abstract class BrowserContext extends SdkObject {
await frame.goto(metadata, originState.origin, { timeout: 0 });
const restoreScript = `(() => {
const module = {};
${js.prepareGeneratedScript(rawStorageSource.source)}
${rawStorageSource.source}
const script = new (module.exports.StorageScript())(${this._browser.options.name === 'firefox'});
return script.restore(${JSON.stringify(originState)});
})()`;

View File

@ -26,8 +26,7 @@ import * as dom from '../dom';
import * as frames from '../frames';
import { helper } from '../helper';
import * as network from '../network';
import { kPlaywrightBinding } from '../javascript';
import { Page, Worker } from '../page';
import { Page, PageBinding, Worker } from '../page';
import { registry } from '../registry';
import { getAccessibilityTree } from './crAccessibility';
import { CRBrowserContext } from './crBrowser';
@ -1071,7 +1070,7 @@ class FrameSession {
}
async exposePlaywrightBinding() {
await this._client.send('Runtime.addBinding', { name: kPlaywrightBinding });
await this._client.send('Runtime.addBinding', { name: PageBinding.kBindingName });
}
async _getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {

View File

@ -15,7 +15,6 @@
*/
import * as rawClockSource from '../generated/clockSource';
import { prepareGeneratedScript } from './javascript';
import type { BrowserContext } from './browserContext';
@ -85,7 +84,7 @@ export class Clock {
this._scriptInstalled = true;
const script = `(() => {
const module = {};
${prepareGeneratedScript(rawClockSource.source)}
${rawClockSource.source}
globalThis.__pwClock = (module.exports.inject())(globalThis);
})();`;
await this._browserContext.addInitScript(script);

View File

@ -18,7 +18,6 @@ import { Page } from '../page';
import { Dispatcher } from './dispatcher';
import { PageDispatcher } from './pageDispatcher';
import * as rawWebSocketMockSource from '../../generated/webSocketMockSource';
import { prepareGeneratedScript } from '../javascript';
import { createGuid } from '../utils/crypto';
import { urlMatches } from '../../utils/isomorphic/urlMatch';
import { eventsHelper } from '../utils/eventsHelper';
@ -97,7 +96,7 @@ export class WebSocketRouteDispatcher extends Dispatcher<{ guid: string }, chann
await target.addInitScript(`
(() => {
const module = {};
${prepareGeneratedScript(rawWebSocketMockSource.source)}
${rawWebSocketMockSource.source}
(module.exports.inject())(globalThis);
})();
`, kInitScriptName);

View File

@ -18,7 +18,7 @@ import fs from 'fs';
import * as js from './javascript';
import { ProgressController } from './progress';
import { asLocator } from '../utils';
import { asLocator, isUnderTest } from '../utils';
import { prepareFilesForUpload } from './fileUploadUtils';
import { isSessionClosedError } from './protocolError';
import * as rawInjectedScriptSource from '../generated/injectedScriptSource';
@ -89,6 +89,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
customEngines.push({ name, source });
const sdkLanguage = this.frame.attribution.playwright.options.sdkLanguage;
const options: InjectedScriptOptions = {
isUnderTest: isUnderTest(),
sdkLanguage,
testIdAttributeName: selectorsRegistry.testIdAttributeName(),
stableRafCount: this.frame._page.delegate.rafCountForStablePosition(),
@ -99,7 +100,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
const source = `
(() => {
const module = {};
${js.prepareGeneratedScript(rawInjectedScriptSource.source)}
${rawInjectedScriptSource.source}
return new (module.exports.InjectedScript())(globalThis, ${JSON.stringify(options)});
})();
`;

View File

@ -19,10 +19,10 @@ import { assert } from '../../utils';
import { Browser } from '../browser';
import { BrowserContext, verifyGeolocation } from '../browserContext';
import { TargetClosedError } from '../errors';
import { kPlaywrightBinding } from '../javascript';
import * as network from '../network';
import { ConnectionEvents, FFConnection } from './ffConnection';
import { FFPage } from './ffPage';
import { PageBinding } from '../page';
import type { BrowserOptions } from '../browser';
import type { SdkObject } from '../instrumentation';
@ -393,7 +393,7 @@ export class FFBrowserContext extends BrowserContext {
}
override async doExposePlaywrightBinding() {
this._browser.session.send('Browser.addBinding', { browserContextId: this._browserContextId, name: kPlaywrightBinding, script: '' });
this._browser.session.send('Browser.addBinding', { browserContextId: this._browserContextId, name: PageBinding.kBindingName, script: '' });
}
onClosePersistent() {}

View File

@ -16,40 +16,13 @@
import { SdkObject } from './instrumentation';
import * as rawUtilityScriptSource from '../generated/utilityScriptSource';
import { createGuid, isUnderTest } from '../utils';
import { isUnderTest } from '../utils';
import { serializeAsCallArgument } from '../utils/isomorphic/utilityScriptSerializers';
import { LongStandingScope } from '../utils/isomorphic/manualPromise';
import type * as dom from './dom';
import type { UtilityScript } from '@injected/utilityScript';
// --- This section should match utilityScript.ts and generated_injected_builtins.js ---
// Use in the web-facing names to avoid leaking Playwright to the pages.
export const runtimeGuid = createGuid();
// Preprocesses any generated script to include the runtime guid.
export function prepareGeneratedScript(source: string) {
return source.replaceAll('$runtime_guid$', runtimeGuid).replace('kUtilityScriptIsUnderTest = false', `kUtilityScriptIsUnderTest = ${isUnderTest()}`);
}
export const kUtilityScriptSource = prepareGeneratedScript(rawUtilityScriptSource.source);
// Include this code in any evaluated source to get access to the UtilityScript instance.
export function accessUtilityScript() {
return `globalThis['__playwright_utility_script__${runtimeGuid}']`;
}
// The name of the global playwright binding, accessed by UtilityScript.
export const kPlaywrightBinding = '__playwright__binding__' + runtimeGuid;
// Include this code in any evaluated source to get access to the BindingsController instance.
export function accessBindingsController() {
return `globalThis['__playwright__binding__controller__${runtimeGuid}']`;
}
// --- End of the matching section ---
interface TaggedAsJSHandle<T> {
__jshandle: T;
}
@ -130,8 +103,8 @@ export class ExecutionContext extends SdkObject {
const source = `
(() => {
const module = {};
${kUtilityScriptSource}
return new (module.exports.UtilityScript())(globalThis);
${rawUtilityScriptSource.source}
return new (module.exports.UtilityScript())(globalThis, ${isUnderTest()});
})();`;
this._utilityScriptPromise = this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(this, source))
.then(handle => {

View File

@ -35,6 +35,7 @@ import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser';
import { ManualPromise } from '../utils/isomorphic/manualPromise';
import { parseEvaluationResultValue } from '../utils/isomorphic/utilityScriptSerializers';
import { compressCallLog } from './callLog';
import * as rawBindingsControllerSource from '../generated/bindingsControllerSource';
import type { Artifact } from './artifact';
import type * as dom from './dom';
@ -849,6 +850,20 @@ export class Worker extends SdkObject {
}
export class PageBinding {
private static kController = '__playwright__binding__controller__';
static kBindingName = '__playwright__binding__';
static createInitScript() {
return new InitScript(`
(() => {
const module = {};
${rawBindingsControllerSource.source}
const property = '${PageBinding.kController}';
if (!globalThis[property])
globalThis[property] = new (module.exports.BindingsController())(globalThis, '${PageBinding.kBindingName}');
})();
`, true /* internal */);
}
readonly name: string;
readonly playwrightFunction: frames.FunctionWithSource;
@ -859,7 +874,7 @@ export class PageBinding {
constructor(name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean) {
this.name = name;
this.playwrightFunction = playwrightFunction;
this.initScript = new InitScript(`${js.accessBindingsController()}.addBinding(${JSON.stringify(name)}, ${needsHandle})`, true /* internal */);
this.initScript = new InitScript(`globalThis['${PageBinding.kController}'].addBinding(${JSON.stringify(name)}, ${needsHandle})`, true /* internal */);
this.needsHandle = needsHandle;
this.internal = name.startsWith('__pw');
}
@ -873,7 +888,7 @@ export class PageBinding {
throw new Error(`Function "${name}" is not exposed`);
let result: any;
if (binding.needsHandle) {
const handle = await context.evaluateExpressionHandle(`arg => ${js.accessBindingsController()}.takeBindingHandle(arg)`, { isFunction: true }, { name, seq }).catch(e => null);
const handle = await context.evaluateExpressionHandle(`arg => globalThis['${PageBinding.kController}'].takeBindingHandle(arg)`, { isFunction: true }, { name, seq }).catch(e => null);
result = await binding.playwrightFunction({ frame: context.frame, page, context: page.browserContext }, handle);
} else {
if (!Array.isArray(serializedArgs))
@ -881,9 +896,9 @@ export class PageBinding {
const args = serializedArgs!.map(a => parseEvaluationResultValue(a));
result = await binding.playwrightFunction({ frame: context.frame, page, context: page.browserContext }, ...args);
}
context.evaluateExpressionHandle(`arg => ${js.accessBindingsController()}.deliverBindingResult(arg)`, { isFunction: true }, { name, seq, result }).catch(e => debugLogger.log('error', e));
context.evaluateExpressionHandle(`arg => globalThis['${PageBinding.kController}'].deliverBindingResult(arg)`, { isFunction: true }, { name, seq, result }).catch(e => debugLogger.log('error', e));
} catch (error) {
context.evaluateExpressionHandle(`arg => ${js.accessBindingsController()}.deliverBindingResult(arg)`, { isFunction: true }, { name, seq, error }).catch(e => debugLogger.log('error', e));
context.evaluateExpressionHandle(`arg => globalThis['${PageBinding.kController}'].deliverBindingResult(arg)`, { isFunction: true }, { name, seq, error }).catch(e => debugLogger.log('error', e));
}
}
}

View File

@ -26,7 +26,6 @@ import { Frame } from '../frames';
import { Page } from '../page';
import { ThrottledFile } from './throttledFile';
import { generateCode } from '../codegen/language';
import { prepareGeneratedScript } from '../javascript';
import type { RegisteredListener } from '../../utils';
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from '../codegen/types';
@ -147,7 +146,7 @@ export class ContextRecorder extends EventEmitter {
await this._context.exposeBinding('__pw_recorderRecordAction', false,
(source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action));
await this._context.extendInjectedScript(prepareGeneratedScript(rawRecorderSource.source));
await this._context.extendInjectedScript(rawRecorderSource.source);
}
setEnabled(enabled: boolean) {

View File

@ -30,8 +30,7 @@ import * as dom from '../dom';
import { TargetClosedError } from '../errors';
import { helper } from '../helper';
import * as network from '../network';
import { kPlaywrightBinding } from '../javascript';
import { Page } from '../page';
import { Page, PageBinding } from '../page';
import { getAccessibilityTree } from './wkAccessibility';
import { WKSession } from './wkConnection';
import { createHandle, WKExecutionContext } from './wkExecutionContext';
@ -182,7 +181,7 @@ export class WKPage implements PageDelegate {
this._workers.initializeSession(session)
];
if (this._page.browserContext.needsPlaywrightBinding())
promises.push(session.send('Runtime.addBinding', { name: kPlaywrightBinding }));
promises.push(session.send('Runtime.addBinding', { name: PageBinding.kBindingName }));
if (this._page.needsRequestInterception()) {
promises.push(session.send('Network.setInterceptionEnabled', { enabled: true }));
promises.push(session.send('Network.setResourceCachingDisabled', { disabled: true }));
@ -774,7 +773,7 @@ export class WKPage implements PageDelegate {
}
async exposePlaywrightBinding() {
await this._updateState('Runtime.addBinding', { name: kPlaywrightBinding });
await this._updateState('Runtime.addBinding', { name: PageBinding.kBindingName });
}
private _calculateBootstrapScript(): string {

View File

@ -144,9 +144,6 @@ const inlineCSSPlugin = {
let content = await fs.promises.readFile(outFileJs, 'utf-8');
if (hasExports)
content = await replaceEsbuildHeader(content, outFileJs);
if (injected.endsWith('utilityScript.ts') && !content.includes('kUtilityScriptIsUnderTest = false')) {
throw new Error(`Utility script must include "kUtilityScriptIsUnderTest = false"\n\n${content}`);
}
const newContent = `export const source = ${JSON.stringify(content)};`;
await fs.promises.writeFile(path.join(generatedFolder, baseName.replace('.ts', 'Source.ts')), newContent);
}