chore: do not leak pw internals (#35721)

This commit is contained in:
Pavel Feldman 2025-04-23 21:53:17 -07:00 committed by GitHub
parent 400f466957
commit 3371fb9bea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 106 additions and 69 deletions

View File

@ -44,6 +44,7 @@ import type { LayoutSelectorName } from './layoutSelectorUtils';
import type { SelectorEngine, SelectorRoot } from './selectorEngine'; import type { SelectorEngine, SelectorRoot } from './selectorEngine';
import type { GenerateSelectorOptions } from './selectorGenerator'; import type { GenerateSelectorOptions } from './selectorGenerator';
import type { ElementText, TextMatcher } from './selectorUtils'; import type { ElementText, TextMatcher } from './selectorUtils';
import type { Builtins } from '@isomorphic/builtins';
export type FrameExpectParams = Omit<channels.FrameExpectParams, 'expectedValue'> & { expectedValue?: any }; export type FrameExpectParams = Omit<channels.FrameExpectParams, 'expectedValue'> & { 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; 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 { export class InjectedScript {
private _engines: Map<string, SelectorEngine>; private _engines: Map<string, SelectorEngine>;
readonly _evaluator: SelectorEvaluatorImpl; readonly _evaluator: SelectorEvaluatorImpl;
@ -96,7 +109,7 @@ export class InjectedScript {
isInsideScope, isInsideScope,
normalizeWhiteSpace, normalizeWhiteSpace,
parseAriaSnapshot, parseAriaSnapshot,
builtins: builtins(), builtins: null as unknown as Builtins,
}; };
private _autoClosingTags: Set<string>; private _autoClosingTags: Set<string>;
@ -108,15 +121,15 @@ export class InjectedScript {
private _allHitTargetInterceptorEvents: Set<string>; private _allHitTargetInterceptorEvents: Set<string>;
// eslint-disable-next-line no-restricted-globals // 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.window = window;
this.document = window.document; this.document = window.document;
this.isUnderTest = isUnderTest; this.isUnderTest = options.isUnderTest;
// Make sure builtins are created from "window". This is important for InjectedScript instantiated // Make sure builtins are created from "window". This is important for InjectedScript instantiated
// inside a trace viewer snapshot, where "window" differs from "globalThis". // inside a trace viewer snapshot, where "window" differs from "globalThis".
this.utils.builtins = builtins(window); this.utils.builtins = builtins(options.runtimeGuid, window);
this._sdkLanguage = sdkLanguage; this._sdkLanguage = options.sdkLanguage;
this._testIdAttributeNameForStrictErrorAndConsoleCodegen = testIdAttributeNameForStrictErrorAndConsoleCodegen; this._testIdAttributeNameForStrictErrorAndConsoleCodegen = options.testIdAttributeName;
this._evaluator = new SelectorEvaluatorImpl(); this._evaluator = new SelectorEvaluatorImpl();
this.consoleApi = new ConsoleAPI(this); this.consoleApi = new ConsoleAPI(this);
@ -216,17 +229,17 @@ export class InjectedScript {
this._engines.set('internal:role', createRoleEngine(true)); this._engines.set('internal:role', createRoleEngine(true));
this._engines.set('aria-ref', this._createAriaIdEngine()); this._engines.set('aria-ref', this._createAriaIdEngine());
for (const { name, engine } of customEngines) for (const { name, source } of options.customEngines)
this._engines.set(name, engine); this._engines.set(name, this.eval(source));
this._stableRafCount = stableRafCount; this._stableRafCount = options.stableRafCount;
this._browserName = browserName; this._browserName = options.browserName;
setGlobalOptions({ browserNameForWorkarounds: browserName, inputFileRoleTextbox }); setGlobalOptions({ browserNameForWorkarounds: options.browserName, inputFileRoleTextbox: options.inputFileRoleTextbox });
this._setupGlobalListenersRemovalDetection(); this._setupGlobalListenersRemovalDetection();
this._setupHitTargetInterceptors(); this._setupHitTargetInterceptors();
if (isUnderTest) if (options.isUnderTest)
(this.window as any).__injectedScript = this; (this.window as any).__injectedScript = this;
} }

View File

@ -17,13 +17,19 @@
import { builtins } from '@isomorphic/builtins'; import { builtins } from '@isomorphic/builtins';
import { source } from '@isomorphic/utilityScriptSerializers'; import { source } from '@isomorphic/utilityScriptSerializers';
import type { Builtins } from '@isomorphic/builtins';
export class UtilityScript { export class UtilityScript {
constructor(isUnderTest: boolean) {
private _builtins: Builtins;
constructor(runtimeGuid: string, isUnderTest: boolean) {
if (isUnderTest) { if (isUnderTest) {
// eslint-disable-next-line no-restricted-globals // 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.serializeAsCallArgument = result.serializeAsCallArgument;
this.parseEvaluationResultValue = result.parseEvaluationResultValue; this.parseEvaluationResultValue = result.parseEvaluationResultValue;
} }
@ -38,7 +44,7 @@ export class UtilityScript {
for (let i = 0; i < args.length; i++) for (let i = 0; i < args.length; i++)
parameters[i] = this.parseEvaluationResultValue(args[i], handles); parameters[i] = this.parseEvaluationResultValue(args[i], handles);
let result = builtins().eval(expression); let result = this._builtins.eval(expression);
if (isFunction === true) { if (isFunction === true) {
result = result(...parameters); result = result(...parameters);
} else if (isFunction === false) { } else if (isFunction === false) {

View File

@ -19,7 +19,7 @@ import { eventsHelper } from '../utils/eventsHelper';
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
import * as dialog from '../dialog'; import * as dialog from '../dialog';
import * as dom from '../dom'; import * as dom from '../dom';
import { Page } from '../page'; import { Page, PageBinding } from '../page';
import { BidiExecutionContext, createHandle } from './bidiExecutionContext'; import { BidiExecutionContext, createHandle } from './bidiExecutionContext';
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './bidiInput'; import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './bidiInput';
import { BidiNetworkManager } from './bidiNetworkManager'; import { BidiNetworkManager } from './bidiNetworkManager';
@ -573,7 +573,7 @@ export class BidiPage implements PageDelegate {
} }
export function addMainBinding(callback: (arg: any) => void) { 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 { function toBidiExecutionContext(executionContext: dom.FrameExecutionContext): BidiExecutionContext {

View File

@ -28,7 +28,7 @@ import { mkdirIfNeeded } from './utils/fileUtils';
import { HarRecorder } from './har/harRecorder'; import { HarRecorder } from './har/harRecorder';
import { helper } from './helper'; import { helper } from './helper';
import { SdkObject, serverSideCallMetadata } from './instrumentation'; 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 utilityScriptSerializers from '../utils/isomorphic/utilityScriptSerializers';
import * as network from './network'; import * as network from './network';
import { InitScript } from './page'; import { InitScript } from './page';
@ -37,6 +37,7 @@ import { Recorder } from './recorder';
import { RecorderApp } from './recorder/recorderApp'; import { RecorderApp } from './recorder/recorderApp';
import * as storageScript from './storageScript'; import * as storageScript from './storageScript';
import { Tracing } from './trace/recorder/tracing'; import { Tracing } from './trace/recorder/tracing';
import * as js from './javascript';
import type { Artifact } from './artifact'; import type { Artifact } from './artifact';
import type { Browser, BrowserOptions } from './browser'; import type { Browser, BrowserOptions } from './browser';
@ -518,7 +519,7 @@ export abstract class BrowserContext extends SdkObject {
}; };
const originsToSave = new Set(this._origins); 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. // First try collecting storage stage from existing pages.
for (const page of this.pages()) { for (const page of this.pages()) {
@ -611,7 +612,7 @@ export abstract class BrowserContext extends SdkObject {
for (const originState of state.origins) { for (const originState of state.origins) {
const frame = page.mainFrame(); const frame = page.mainFrame();
await frame.goto(metadata, originState.origin); 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); await page.close(internalMetadata);
} }

View File

@ -24,7 +24,7 @@ import { isSessionClosedError } from './protocolError';
import * as injectedScriptSource from '../generated/injectedScriptSource'; import * as injectedScriptSource from '../generated/injectedScriptSource';
import type * as frames from './frames'; 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 { CallMetadata } from './instrumentation';
import type { Page } from './page'; import type { Page } from './page';
import type { Progress } from './progress'; import type { Progress } from './progress';
@ -85,25 +85,26 @@ export class FrameExecutionContext extends js.ExecutionContext {
injectedScript(): Promise<js.JSHandle<InjectedScript>> { injectedScript(): Promise<js.JSHandle<InjectedScript>> {
if (!this._injectedScriptPromise) { if (!this._injectedScriptPromise) {
const custom: string[] = []; const customEngines: InjectedScriptOptions['customEngines'] = [];
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}) }`); customEngines.push({ name, source });
const sdkLanguage = this.frame.attribution.playwright.options.sdkLanguage; 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 source = `
(() => { (() => {
const module = {}; const module = {};
${injectedScriptSource.source} ${injectedScriptSource.source}
return new (module.exports.InjectedScript())( return new (module.exports.InjectedScript())(globalThis, ${JSON.stringify(options)});
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')}]
);
})(); })();
`; `;
this._injectedScriptPromise = this.rawEvaluateHandle(source) this._injectedScriptPromise = this.rawEvaluateHandle(source)

View File

@ -16,7 +16,7 @@
import { SdkObject } from './instrumentation'; import { SdkObject } from './instrumentation';
import * as utilityScriptSource from '../generated/utilityScriptSource'; import * as utilityScriptSource from '../generated/utilityScriptSource';
import { isUnderTest } from '../utils'; import { createGuid, isUnderTest } from '../utils';
import { builtins } from '../utils/isomorphic/builtins'; import { builtins } from '../utils/isomorphic/builtins';
import { source } from '../utils/isomorphic/utilityScriptSerializers'; import { source } from '../utils/isomorphic/utilityScriptSerializers';
import { LongStandingScope } from '../utils/isomorphic/manualPromise'; import { LongStandingScope } from '../utils/isomorphic/manualPromise';
@ -24,6 +24,9 @@ import { LongStandingScope } from '../utils/isomorphic/manualPromise';
import type * as dom from './dom'; import type * as dom from './dom';
import type { UtilityScript } from '@injected/utilityScript'; 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<T> { interface TaggedAsJSHandle<T> {
__jshandle: T; __jshandle: T;
} }
@ -46,7 +49,7 @@ export type Func1<Arg, R> = string | ((arg: Unboxed<Arg>) => R | Promise<R>);
export type FuncOn<On, Arg2, R> = string | ((on: On, arg2: Unboxed<Arg2>) => R | Promise<R>); export type FuncOn<On, Arg2, R> = string | ((on: On, arg2: Unboxed<Arg2>) => R | Promise<R>);
export type SmartHandle<T> = T extends Node ? dom.ElementHandle<T> : JSHandle<T>; export type SmartHandle<T> = T extends Node ? dom.ElementHandle<T> : JSHandle<T>;
const utilityScriptSerializers = source(builtins()); const utilityScriptSerializers = source(builtins(runtimeGuid));
export const parseEvaluationResultValue = utilityScriptSerializers.parseEvaluationResultValue; export const parseEvaluationResultValue = utilityScriptSerializers.parseEvaluationResultValue;
export const serializeAsCallArgument = utilityScriptSerializers.serializeAsCallArgument; export const serializeAsCallArgument = utilityScriptSerializers.serializeAsCallArgument;
@ -109,7 +112,7 @@ export class ExecutionContext extends SdkObject {
(() => { (() => {
const module = {}; const module = {};
${utilityScriptSource.source} ${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)) this._utilityScriptPromise = this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(this, source))
.then(handle => { .then(handle => {

View File

@ -24,7 +24,7 @@ import * as frames from './frames';
import { helper } from './helper'; import { helper } from './helper';
import * as input from './input'; import * as input from './input';
import { SdkObject } from './instrumentation'; import { SdkObject } from './instrumentation';
import { builtins } from '../utils/isomorphic/builtins'; import { builtinsSource } from '../utils/isomorphic/builtins';
import { createPageBindingScript, deliverBindingResult, takeBindingHandle } from './pageBinding'; import { createPageBindingScript, deliverBindingResult, takeBindingHandle } from './pageBinding';
import * as js from './javascript'; import * as js from './javascript';
import { ProgressController } from './progress'; import { ProgressController } from './progress';
@ -858,7 +858,7 @@ export class Worker extends SdkObject {
} }
export class PageBinding { export class PageBinding {
static kPlaywrightBinding = '__playwright__binding__'; static kPlaywrightBinding = '__playwright__binding__' + js.runtimeGuid;
readonly name: string; readonly name: string;
readonly playwrightFunction: frames.FunctionWithSource; readonly playwrightFunction: frames.FunctionWithSource;
@ -906,11 +906,15 @@ export class InitScript {
constructor(source: string, internal?: boolean, name?: string) { constructor(source: string, internal?: boolean, name?: string) {
const guid = createGuid(); const guid = createGuid();
this.source = `(() => { this.source = `(() => {
globalThis.__pwInitScripts = globalThis.__pwInitScripts || {}; const name = '__pw_init_scripts__${js.runtimeGuid}';
const hasInitScript = globalThis.__pwInitScripts[${JSON.stringify(guid)}]; 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) if (hasInitScript)
return; return;
globalThis.__pwInitScripts[${JSON.stringify(guid)}] = true; globalThis[name][${JSON.stringify(guid)}] = true;
${source} ${source}
})();`; })();`;
this.internal = !!internal; 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 { class FrameThrottler {
private _acks: (() => void)[] = []; private _acks: (() => void)[] = [];

View File

@ -14,8 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { builtins } from '../utils/isomorphic/builtins'; import { builtinsSource } from '../utils/isomorphic/builtins';
import { source } from '../utils/isomorphic/utilityScriptSerializers'; import { source } from '../utils/isomorphic/utilityScriptSerializers';
import * as js from './javascript';
import type { Builtins } from '../utils/isomorphic/builtins'; import type { Builtins } from '../utils/isomorphic/builtins';
import type { SerializedValue } from '../utils/isomorphic/utilityScriptSerializers'; 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) { 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)})`;
} }

View File

@ -41,10 +41,14 @@ export type Builtins = {
// anything else happens in the page. This way, original builtins are saved on the global object // 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 // before page can temper with them. Later on, any call to builtins() will retrieve the stored
// builtins instead of initializing them again. // 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; global = global ?? globalThis;
if (!(global as any)['__playwright_builtins__']) { const name = `__playwright_builtins__${runtimeGuid}`;
const builtins: Builtins = { let builtins: Builtins = (global as any)[name];
if (builtins)
return builtins;
builtins = {
setTimeout: global.setTimeout?.bind(global), setTimeout: global.setTimeout?.bind(global),
clearTimeout: global.clearTimeout?.bind(global), clearTimeout: global.clearTimeout?.bind(global),
setInterval: global.setInterval?.bind(global), setInterval: global.setInterval?.bind(global),
@ -60,12 +64,16 @@ export function builtins(global?: typeof globalThis): Builtins {
Map: global.Map, Map: global.Map,
Set: global.Set, Set: global.Set,
}; };
Object.defineProperty(global, '__playwright_builtins__', { value: builtins, configurable: false, enumerable: false, writable: false }); if (runtimeGuid)
} Object.defineProperty(global, name, { value: builtins, configurable: false, enumerable: false, writable: false });
return (global as any)['__playwright_builtins__']; 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 setTimeout = instance.setTimeout;
export const clearTimeout = instance.clearTimeout; export const clearTimeout = instance.clearTimeout;
export const setInterval = instance.setInterval; export const setInterval = instance.setInterval;

View File

@ -77,7 +77,7 @@ export const SnapshotTabsView: React.FunctionComponent<{
<ToolbarButton icon='link-external' title='Open snapshot in a new tab' disabled={!snapshotUrls?.popoutUrl} onClick={() => { <ToolbarButton icon='link-external' title='Open snapshot in a new tab' disabled={!snapshotUrls?.popoutUrl} onClick={() => {
const win = window.open(snapshotUrls?.popoutUrl || '', '_blank'); const win = window.open(snapshotUrls?.popoutUrl || '', '_blank');
win?.addEventListener('DOMContentLoaded', () => { 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(); injectedScript.consoleApi.install();
}); });
}} /> }} />
@ -280,7 +280,7 @@ function createRecorders(recorders: { recorder: Recorder, frameSelector: string
return; return;
const win = frameWindow as any; const win = frameWindow as any;
if (!win._recorder) { 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); const recorder = new Recorder(injectedScript);
win._injectedScript = injectedScript; win._injectedScript = injectedScript;
win._recorder = { recorder, frameSelector: parentFrameSelector }; win._recorder = { recorder, frameSelector: parentFrameSelector };