diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index c644f99353..cc32237827 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -44,6 +44,7 @@ import type { LayoutSelectorName } from './layoutSelectorUtils'; import type { SelectorEngine, SelectorRoot } from './selectorEngine'; import type { GenerateSelectorOptions } from './selectorGenerator'; import type { ElementText, TextMatcher } from './selectorUtils'; +import type { Builtins } from '@isomorphic/builtins'; export type FrameExpectParams = Omit & { expectedValue?: any }; @@ -64,6 +65,18 @@ interface WebKitLegacyDeviceMotionEvent extends DeviceMotionEvent { readonly initDeviceMotionEvent: (type: string, bubbles: boolean, cancelable: boolean, acceleration: DeviceMotionEventAcceleration, accelerationIncludingGravity: DeviceMotionEventAcceleration, rotationRate: DeviceMotionEventRotationRate, interval: number) => void; } +export type InjectedScriptOptions = { + isUnderTest: boolean; + sdkLanguage: Language; + // For strict error and codegen + testIdAttributeName: string; + stableRafCount: number; + browserName: string; + inputFileRoleTextbox: boolean; + customEngines: { name: string, source: string }[]; + runtimeGuid: string; +}; + export class InjectedScript { private _engines: Map; readonly _evaluator: SelectorEvaluatorImpl; @@ -96,7 +109,7 @@ export class InjectedScript { isInsideScope, normalizeWhiteSpace, parseAriaSnapshot, - builtins: builtins(), + builtins: null as unknown as Builtins, }; private _autoClosingTags: Set; @@ -108,15 +121,15 @@ export class InjectedScript { private _allHitTargetInterceptorEvents: Set; // eslint-disable-next-line no-restricted-globals - constructor(window: Window & typeof globalThis, isUnderTest: boolean, sdkLanguage: Language, testIdAttributeNameForStrictErrorAndConsoleCodegen: string, stableRafCount: number, browserName: string, inputFileRoleTextbox: boolean, customEngines: { name: string, engine: SelectorEngine }[]) { + constructor(window: Window & typeof globalThis, options: InjectedScriptOptions) { this.window = window; this.document = window.document; - this.isUnderTest = isUnderTest; + 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". - this.utils.builtins = builtins(window); - this._sdkLanguage = sdkLanguage; - this._testIdAttributeNameForStrictErrorAndConsoleCodegen = testIdAttributeNameForStrictErrorAndConsoleCodegen; + this.utils.builtins = builtins(options.runtimeGuid, window); + this._sdkLanguage = options.sdkLanguage; + this._testIdAttributeNameForStrictErrorAndConsoleCodegen = options.testIdAttributeName; this._evaluator = new SelectorEvaluatorImpl(); this.consoleApi = new ConsoleAPI(this); @@ -216,17 +229,17 @@ export class InjectedScript { this._engines.set('internal:role', createRoleEngine(true)); this._engines.set('aria-ref', this._createAriaIdEngine()); - for (const { name, engine } of customEngines) - this._engines.set(name, engine); + for (const { name, source } of options.customEngines) + this._engines.set(name, this.eval(source)); - this._stableRafCount = stableRafCount; - this._browserName = browserName; - setGlobalOptions({ browserNameForWorkarounds: browserName, inputFileRoleTextbox }); + this._stableRafCount = options.stableRafCount; + this._browserName = options.browserName; + setGlobalOptions({ browserNameForWorkarounds: options.browserName, inputFileRoleTextbox: options.inputFileRoleTextbox }); this._setupGlobalListenersRemovalDetection(); this._setupHitTargetInterceptors(); - if (isUnderTest) + if (options.isUnderTest) (this.window as any).__injectedScript = this; } diff --git a/packages/injected/src/utilityScript.ts b/packages/injected/src/utilityScript.ts index a2526f98db..e07733076f 100644 --- a/packages/injected/src/utilityScript.ts +++ b/packages/injected/src/utilityScript.ts @@ -17,13 +17,19 @@ import { builtins } from '@isomorphic/builtins'; import { source } from '@isomorphic/utilityScriptSerializers'; +import type { Builtins } from '@isomorphic/builtins'; + export class UtilityScript { - constructor(isUnderTest: boolean) { + + private _builtins: Builtins; + + constructor(runtimeGuid: string, isUnderTest: boolean) { if (isUnderTest) { // eslint-disable-next-line no-restricted-globals - (globalThis as any).builtins = builtins(); + (globalThis as any).builtins = builtins(runtimeGuid); } - const result = source(builtins()); + this._builtins = builtins(runtimeGuid); + const result = source(this._builtins); this.serializeAsCallArgument = result.serializeAsCallArgument; this.parseEvaluationResultValue = result.parseEvaluationResultValue; } @@ -38,7 +44,7 @@ export class UtilityScript { for (let i = 0; i < args.length; i++) parameters[i] = this.parseEvaluationResultValue(args[i], handles); - let result = builtins().eval(expression); + let result = this._builtins.eval(expression); if (isFunction === true) { result = result(...parameters); } else if (isFunction === false) { diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index 76267ab89c..50e45d1352 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -19,7 +19,7 @@ import { eventsHelper } from '../utils/eventsHelper'; import { BrowserContext } from '../browserContext'; import * as dialog from '../dialog'; import * as dom from '../dom'; -import { Page } from '../page'; +import { Page, PageBinding } from '../page'; import { BidiExecutionContext, createHandle } from './bidiExecutionContext'; import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './bidiInput'; import { BidiNetworkManager } from './bidiNetworkManager'; @@ -573,7 +573,7 @@ export class BidiPage implements PageDelegate { } export function addMainBinding(callback: (arg: any) => void) { - (globalThis as any)['__playwright__binding__'] = callback; + (globalThis as any)[PageBinding.kPlaywrightBinding] = callback; } function toBidiExecutionContext(executionContext: dom.FrameExecutionContext): BidiExecutionContext { diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 46bc5ad7b9..1c44ac2987 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -28,7 +28,7 @@ import { mkdirIfNeeded } from './utils/fileUtils'; import { HarRecorder } from './har/harRecorder'; import { helper } from './helper'; import { SdkObject, serverSideCallMetadata } from './instrumentation'; -import { builtins } from '../utils/isomorphic/builtins'; +import { builtinsSource } from '../utils/isomorphic/builtins'; import * as utilityScriptSerializers from '../utils/isomorphic/utilityScriptSerializers'; import * as network from './network'; import { InitScript } from './page'; @@ -37,6 +37,7 @@ import { Recorder } from './recorder'; import { RecorderApp } from './recorder/recorderApp'; import * as storageScript from './storageScript'; import { Tracing } from './trace/recorder/tracing'; +import * as js from './javascript'; import type { Artifact } from './artifact'; import type { Browser, BrowserOptions } from './browser'; @@ -518,7 +519,7 @@ export abstract class BrowserContext extends SdkObject { }; const originsToSave = new Set(this._origins); - const collectScript = `(${storageScript.collect})(${utilityScriptSerializers.source}, (${builtins})(), ${this._browser.options.name === 'firefox'}, ${indexedDB})`; + const collectScript = `(${storageScript.collect})(${utilityScriptSerializers.source}, ${builtinsSource(js.runtimeGuid)}, ${this._browser.options.name === 'firefox'}, ${indexedDB})`; // First try collecting storage stage from existing pages. for (const page of this.pages()) { @@ -611,7 +612,7 @@ export abstract class BrowserContext extends SdkObject { for (const originState of state.origins) { const frame = page.mainFrame(); await frame.goto(metadata, originState.origin); - await frame.evaluateExpression(`(${storageScript.restore})(${utilityScriptSerializers.source}, (${builtins})(), ${JSON.stringify(originState)})`, { world: 'utility' }); + await frame.evaluateExpression(`(${storageScript.restore})(${utilityScriptSerializers.source}, ${builtinsSource(js.runtimeGuid)}, ${JSON.stringify(originState)})`, { world: 'utility' }); } await page.close(internalMetadata); } diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 0886ad9195..a859c7992f 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -24,7 +24,7 @@ import { isSessionClosedError } from './protocolError'; import * as injectedScriptSource from '../generated/injectedScriptSource'; import type * as frames from './frames'; -import type { ElementState, HitTargetInterceptionResult, InjectedScript } from '@injected/injectedScript'; +import type { ElementState, HitTargetInterceptionResult, InjectedScript, InjectedScriptOptions } from '@injected/injectedScript'; import type { CallMetadata } from './instrumentation'; import type { Page } from './page'; import type { Progress } from './progress'; @@ -85,25 +85,26 @@ export class FrameExecutionContext extends js.ExecutionContext { injectedScript(): Promise> { if (!this._injectedScriptPromise) { - const custom: string[] = []; + const customEngines: InjectedScriptOptions['customEngines'] = []; const selectorsRegistry = this.frame._page.context().selectors(); for (const [name, { source }] of selectorsRegistry._engines) - custom.push(`{ name: '${name}', engine: (${source}) }`); + 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(), + browserName: this.frame._page._browserContext._browser.options.name, + inputFileRoleTextbox: process.env.PLAYWRIGHT_INPUT_FILE_TEXTBOX ? true : false, + customEngines, + runtimeGuid: js.runtimeGuid, + }; const source = ` (() => { const module = {}; ${injectedScriptSource.source} - return new (module.exports.InjectedScript())( - globalThis, - ${isUnderTest()}, - "${sdkLanguage}", - ${JSON.stringify(selectorsRegistry.testIdAttributeName())}, - ${this.frame._page._delegate.rafCountForStablePosition()}, - "${this.frame._page._browserContext._browser.options.name}", - ${process.env.PLAYWRIGHT_INPUT_FILE_TEXTBOX ? 'true' : 'false'}, - [${custom.join(',\n')}] - ); + return new (module.exports.InjectedScript())(globalThis, ${JSON.stringify(options)}); })(); `; this._injectedScriptPromise = this.rawEvaluateHandle(source) diff --git a/packages/playwright-core/src/server/javascript.ts b/packages/playwright-core/src/server/javascript.ts index 4413c3f19e..0fce67c26f 100644 --- a/packages/playwright-core/src/server/javascript.ts +++ b/packages/playwright-core/src/server/javascript.ts @@ -16,7 +16,7 @@ import { SdkObject } from './instrumentation'; import * as utilityScriptSource from '../generated/utilityScriptSource'; -import { isUnderTest } from '../utils'; +import { createGuid, isUnderTest } from '../utils'; import { builtins } from '../utils/isomorphic/builtins'; import { source } from '../utils/isomorphic/utilityScriptSerializers'; import { LongStandingScope } from '../utils/isomorphic/manualPromise'; @@ -24,6 +24,9 @@ import { LongStandingScope } from '../utils/isomorphic/manualPromise'; import type * as dom from './dom'; import type { UtilityScript } from '@injected/utilityScript'; +// Use in the web-facing names to avoid leaking Playwright to the pages. +export const runtimeGuid = createGuid(); + interface TaggedAsJSHandle { __jshandle: T; } @@ -46,7 +49,7 @@ export type Func1 = string | ((arg: Unboxed) => R | Promise); export type FuncOn = string | ((on: On, arg2: Unboxed) => R | Promise); export type SmartHandle = T extends Node ? dom.ElementHandle : JSHandle; -const utilityScriptSerializers = source(builtins()); +const utilityScriptSerializers = source(builtins(runtimeGuid)); export const parseEvaluationResultValue = utilityScriptSerializers.parseEvaluationResultValue; export const serializeAsCallArgument = utilityScriptSerializers.serializeAsCallArgument; @@ -109,7 +112,7 @@ export class ExecutionContext extends SdkObject { (() => { const module = {}; ${utilityScriptSource.source} - return new (module.exports.UtilityScript())(${isUnderTest()}); + return new (module.exports.UtilityScript())(${JSON.stringify(runtimeGuid)}, ${isUnderTest()}); })();`; this._utilityScriptPromise = this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(this, source)) .then(handle => { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index d10a638e6c..4e739c6493 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -24,7 +24,7 @@ import * as frames from './frames'; import { helper } from './helper'; import * as input from './input'; import { SdkObject } from './instrumentation'; -import { builtins } from '../utils/isomorphic/builtins'; +import { builtinsSource } from '../utils/isomorphic/builtins'; import { createPageBindingScript, deliverBindingResult, takeBindingHandle } from './pageBinding'; import * as js from './javascript'; import { ProgressController } from './progress'; @@ -858,7 +858,7 @@ export class Worker extends SdkObject { } export class PageBinding { - static kPlaywrightBinding = '__playwright__binding__'; + static kPlaywrightBinding = '__playwright__binding__' + js.runtimeGuid; readonly name: string; readonly playwrightFunction: frames.FunctionWithSource; @@ -906,11 +906,15 @@ export class InitScript { constructor(source: string, internal?: boolean, name?: string) { const guid = createGuid(); this.source = `(() => { - globalThis.__pwInitScripts = globalThis.__pwInitScripts || {}; - const hasInitScript = globalThis.__pwInitScripts[${JSON.stringify(guid)}]; + const name = '__pw_init_scripts__${js.runtimeGuid}'; + if (!globalThis[name]) + Object.defineProperty(globalThis, name, { value: {}, configurable: false, enumerable: false, writable: false }); + + const globalInitScripts = globalThis[name]; + const hasInitScript = globalInitScripts[${JSON.stringify(guid)}]; if (hasInitScript) return; - globalThis.__pwInitScripts[${JSON.stringify(guid)}] = true; + globalThis[name][${JSON.stringify(guid)}] = true; ${source} })();`; this.internal = !!internal; @@ -918,7 +922,7 @@ export class InitScript { } } -export const kBuiltinsScript = new InitScript(`(${builtins})()`, true /* internal */); +export const kBuiltinsScript = new InitScript(builtinsSource(js.runtimeGuid), true /* internal */); class FrameThrottler { private _acks: (() => void)[] = []; diff --git a/packages/playwright-core/src/server/pageBinding.ts b/packages/playwright-core/src/server/pageBinding.ts index 0e7f1da8d6..a1910b9f6a 100644 --- a/packages/playwright-core/src/server/pageBinding.ts +++ b/packages/playwright-core/src/server/pageBinding.ts @@ -14,8 +14,9 @@ * limitations under the License. */ -import { builtins } from '../utils/isomorphic/builtins'; +import { builtinsSource } from '../utils/isomorphic/builtins'; import { source } from '../utils/isomorphic/utilityScriptSerializers'; +import * as js from './javascript'; import type { Builtins } from '../utils/isomorphic/builtins'; import type { SerializedValue } from '../utils/isomorphic/utilityScriptSerializers'; @@ -88,5 +89,5 @@ export function deliverBindingResult(arg: { name: string, seq: number, result?: } export function createPageBindingScript(playwrightBinding: string, name: string, needsHandle: boolean) { - return `(${addPageBinding.toString()})(${JSON.stringify(playwrightBinding)}, ${JSON.stringify(name)}, ${needsHandle}, (${source}), (${builtins})())`; + return `(${addPageBinding.toString()})(${JSON.stringify(playwrightBinding)}, ${JSON.stringify(name)}, ${needsHandle}, (${source}), ${builtinsSource(js.runtimeGuid)})`; } diff --git a/packages/playwright-core/src/utils/isomorphic/builtins.ts b/packages/playwright-core/src/utils/isomorphic/builtins.ts index 7a4d2502b6..fa80287cf4 100644 --- a/packages/playwright-core/src/utils/isomorphic/builtins.ts +++ b/packages/playwright-core/src/utils/isomorphic/builtins.ts @@ -41,31 +41,39 @@ export type Builtins = { // anything else happens in the page. This way, original builtins are saved on the global object // before page can temper with them. Later on, any call to builtins() will retrieve the stored // builtins instead of initializing them again. -export function builtins(global?: typeof globalThis): Builtins { +export function builtins(runtimeGuid: string, global?: typeof globalThis): Builtins { global = global ?? globalThis; - if (!(global as any)['__playwright_builtins__']) { - const builtins: Builtins = { - setTimeout: global.setTimeout?.bind(global), - clearTimeout: global.clearTimeout?.bind(global), - setInterval: global.setInterval?.bind(global), - clearInterval: global.clearInterval?.bind(global), - requestAnimationFrame: global.requestAnimationFrame?.bind(global), - cancelAnimationFrame: global.cancelAnimationFrame?.bind(global), - requestIdleCallback: global.requestIdleCallback?.bind(global), - cancelIdleCallback: global.cancelIdleCallback?.bind(global), - performance: global.performance, - eval: global.eval?.bind(global), - Intl: global.Intl, - Date: global.Date, - Map: global.Map, - Set: global.Set, - }; - Object.defineProperty(global, '__playwright_builtins__', { value: builtins, configurable: false, enumerable: false, writable: false }); - } - return (global as any)['__playwright_builtins__']; + const name = `__playwright_builtins__${runtimeGuid}`; + let builtins: Builtins = (global as any)[name]; + if (builtins) + return builtins; + + builtins = { + setTimeout: global.setTimeout?.bind(global), + clearTimeout: global.clearTimeout?.bind(global), + setInterval: global.setInterval?.bind(global), + clearInterval: global.clearInterval?.bind(global), + requestAnimationFrame: global.requestAnimationFrame?.bind(global), + cancelAnimationFrame: global.cancelAnimationFrame?.bind(global), + requestIdleCallback: global.requestIdleCallback?.bind(global), + cancelIdleCallback: global.cancelIdleCallback?.bind(global), + performance: global.performance, + eval: global.eval?.bind(global), + Intl: global.Intl, + Date: global.Date, + Map: global.Map, + Set: global.Set, + }; + if (runtimeGuid) + Object.defineProperty(global, name, { value: builtins, configurable: false, enumerable: false, writable: false }); + return builtins; } -const instance = builtins(); +export function builtinsSource(runtimeGuid: string): string { + return `(${builtins})(${JSON.stringify(runtimeGuid)})`; +} + +const instance = builtins(''); export const setTimeout = instance.setTimeout; export const clearTimeout = instance.clearTimeout; export const setInterval = instance.setInterval; diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 7211aa9e2e..de4d4bb4e0 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -77,7 +77,7 @@ export const SnapshotTabsView: React.FunctionComponent<{ { const win = window.open(snapshotUrls?.popoutUrl || '', '_blank'); win?.addEventListener('DOMContentLoaded', () => { - const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', false, []); + const injectedScript = new InjectedScript(win as any, { isUnderTest: false, sdkLanguage, testIdAttributeName, stableRafCount: 1, browserName: 'chromium', inputFileRoleTextbox: false, customEngines: [], runtimeGuid: '' }); injectedScript.consoleApi.install(); }); }} /> @@ -280,7 +280,7 @@ function createRecorders(recorders: { recorder: Recorder, frameSelector: string return; const win = frameWindow as any; if (!win._recorder) { - const injectedScript = new InjectedScript(frameWindow as any, isUnderTest, sdkLanguage, testIdAttributeName, 1, 'chromium', false, []); + const injectedScript = new InjectedScript(frameWindow as any, { isUnderTest, sdkLanguage, testIdAttributeName, stableRafCount: 1, browserName: 'chromium', inputFileRoleTextbox: false, customEngines: [], runtimeGuid: '' }); const recorder = new Recorder(injectedScript); win._injectedScript = injectedScript; win._recorder = { recorder, frameSelector: parentFrameSelector };