mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: do not leak pw internals (#35721)
This commit is contained in:
parent
400f466957
commit
3371fb9bea
@ -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<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;
|
||||
}
|
||||
|
||||
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<string, SelectorEngine>;
|
||||
readonly _evaluator: SelectorEvaluatorImpl;
|
||||
@ -96,7 +109,7 @@ export class InjectedScript {
|
||||
isInsideScope,
|
||||
normalizeWhiteSpace,
|
||||
parseAriaSnapshot,
|
||||
builtins: builtins(),
|
||||
builtins: null as unknown as Builtins,
|
||||
};
|
||||
|
||||
private _autoClosingTags: Set<string>;
|
||||
@ -108,15 +121,15 @@ export class InjectedScript {
|
||||
private _allHitTargetInterceptorEvents: Set<string>;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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<js.JSHandle<InjectedScript>> {
|
||||
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)
|
||||
|
@ -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<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 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 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 => {
|
||||
|
@ -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)[] = [];
|
||||
|
@ -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)})`;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -77,7 +77,7 @@ export const SnapshotTabsView: React.FunctionComponent<{
|
||||
<ToolbarButton icon='link-external' title='Open snapshot in a new tab' disabled={!snapshotUrls?.popoutUrl} onClick={() => {
|
||||
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 };
|
||||
|
Loading…
x
Reference in New Issue
Block a user