chore: inject builtins through esbuild (#35800)

This commit is contained in:
Dmitry Gozman 2025-04-29 19:07:06 +00:00 committed by GitHub
parent 0766aa9afb
commit 3b0135411e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 313 additions and 329 deletions

View File

@ -217,24 +217,10 @@ const noBooleanCompareRules = {
"@typescript-eslint/no-unnecessary-boolean-literal-compare": 2,
};
// This should contain every builtin from builtins.ts.
const noWebGlobalsRuleList = [
{ name: "window", message: "Use InjectedScript.window instead" },
{ name: "document", message: "Use InjectedScript.document instead" },
{ name: "globalThis", message: "Use InjectedScript.window instead" },
{ name: "setTimeout", message: "import { setTimeout } from './builtins' instead" },
{ name: "clearTimeout", message: "import { clearTimeout } from './builtins' instead" },
{ name: "setInterval", message: "import { setInterval } from './builtins' instead" },
{ name: "clearInterval", message: "import { clearInterval } from './builtins' instead" },
{ name: "requestAnimationFrame", message: "import { requestAnimationFrame } from './builtins' instead" },
{ name: "cancelAnimationFrame", message: "import { cancelAnimationFrame } from './builtins' instead" },
{ name: "requestIdleCallback", message: "import { requestIdleCallback } from './builtins' instead" },
{ name: "cancelIdleCallback", message: "import { cancelIdleCallback } from './builtins' instead" },
{ name: "performance", message: "import { performance } from './builtins' instead" },
{ name: "eval", message: "Use builtins().eval instead" },
{ name: "Date", message: "import { Date } from './builtins' instead" },
{ name: "Map", message: "import { Map } from './builtins' instead" },
{ name: "Set", message: "import { Set } from './builtins' instead" },
];
const noNodeGlobalsRuleList = [{ name: "process" }];
@ -380,7 +366,6 @@ export default [
{
files: [
"packages/injected/src/**/*.ts",
"packages/playwright-core/src/server/pageBinding.ts",
"packages/playwright-core/src/server/storageScript.ts",
],
languageOptions: languageOptionsWithTsConfig,

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Map, Set } from '@isomorphic/builtins';
import { escapeRegExp, longestCommonSubstring, normalizeWhiteSpace } from '@isomorphic/stringUtils';
import { box, getElementComputedStyle, getGlobalOptions } from './domUtils';

View File

@ -10,8 +10,6 @@
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { Map, Date } from '@isomorphic/builtins';
export type ClockMethods = {
Date: DateConstructor;
setTimeout: Window['setTimeout'];
@ -693,7 +691,6 @@ export function createClock(globalObject: WindowOrWorkerGlobalScope): { clock: C
},
};
// TODO: unify Builtins and platformOriginals
const clock = new ClockController(embedder);
const api = createApi(clock, originals.bound);
return { clock, api, originals: originals.raw };

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { requestAnimationFrame, cancelAnimationFrame } from '@isomorphic/builtins';
import { asLocator } from '@isomorphic/locatorGenerators';
import { stringifySelector } from '@isomorphic/selectorParser';

View File

@ -15,7 +15,6 @@
*/
import { parseAriaSnapshot } from '@isomorphic/ariaSnapshot';
import { builtins, Set, Map, requestAnimationFrame, performance } from '@isomorphic/builtins';
import { asLocator } from '@isomorphic/locatorGenerators';
import { parseAttributeSelector, parseSelector, stringifySelector, visitAllSelectorParts } from '@isomorphic/selectorParser';
import { cacheNormalizedWhitespaces, normalizeWhiteSpace, trimStringWithEllipsis } from '@isomorphic/stringUtils';
@ -33,6 +32,7 @@ import { elementMatchesText, elementText, getElementLabels } from './selectorUti
import { createVueEngine } from './vueSelectorEngine';
import { XPathEngine } from './xpathSelectorEngine';
import { ConsoleAPI } from './consoleApi';
import { ensureUtilityScript } from './utilityScript';
import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot';
import type { CSSComplexSelectorList } from '@isomorphic/cssParser';
@ -44,7 +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';
import type { Builtins } from './utilityScript';
export type FrameExpectParams = Omit<channels.FrameExpectParams, 'expectedValue'> & { expectedValue?: any };
@ -66,7 +66,7 @@ interface WebKitLegacyDeviceMotionEvent extends DeviceMotionEvent {
}
export type InjectedScriptOptions = {
isUnderTest: boolean;
isUnderTest?: boolean;
sdkLanguage: Language;
// For strict error and codegen
testIdAttributeName: string;
@ -74,7 +74,6 @@ export type InjectedScriptOptions = {
browserName: string;
inputFileRoleTextbox: boolean;
customEngines: { name: string, source: string }[];
runtimeGuid: string;
};
export class InjectedScript {
@ -124,10 +123,11 @@ 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".
this.utils.builtins = builtins(options.runtimeGuid, window);
const utilityScript = ensureUtilityScript(window);
this.isUnderTest = options.isUnderTest ?? utilityScript.isUnderTest;
this.utils.builtins = utilityScript.builtins;
this._sdkLanguage = options.sdkLanguage;
this._testIdAttributeNameForStrictErrorAndConsoleCodegen = options.testIdAttributeName;
this._evaluator = new SelectorEvaluatorImpl();
@ -239,7 +239,7 @@ export class InjectedScript {
this._setupGlobalListenersRemovalDetection();
this._setupHitTargetInterceptors();
if (options.isUnderTest)
if (this.isUnderTest)
(this.window as any).__injectedScript = this;
}

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Set } from '@isomorphic/builtins';
import { parseAttributeSelector } from '@isomorphic/selectorParser';
import { isInsideScope } from './domUtils';

View File

@ -23,7 +23,6 @@ import type { ElementText } from '../selectorUtils';
import type * as actions from '@recorder/actions';
import type { ElementInfo, Mode, OverlayState, UIState } from '@recorder/recorderTypes';
import type { Language } from '@isomorphic/locatorGenerators';
import type { Set, Map } from '@isomorphic/builtins';
const HighlightColors = {
multiple: '#f6b26b7f',

View File

@ -14,8 +14,6 @@
* limitations under the License.
*/
import { Map, Set } from '@isomorphic/builtins';
import { getGlobalOptions, closestCrossShadow, elementSafeTagName, enclosingShadowRootOrDocument, getElementComputedStyle, isElementStyleVisibilityVisible, isVisibleTextNode, parentElementOrShadowHost } from './domUtils';
import type { AriaRole } from '@isomorphic/ariaSnapshot';

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Map, Set } from '@isomorphic/builtins';
import { customCSSNames } from '@isomorphic/selectorParser';
import { normalizeWhiteSpace } from '@isomorphic/stringUtils';

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Map, Set } from '@isomorphic/builtins';
import { cssEscape, escapeForAttributeSelector, escapeForTextSelector, escapeRegExp, quoteCSSAttributeValue } from '@isomorphic/stringUtils';
import { closestCrossShadow, isElementVisible, isInsideScope, parentElementOrShadowHost } from './domUtils';

View File

@ -18,7 +18,6 @@ import { normalizeWhiteSpace } from '@isomorphic/stringUtils';
import { getAriaLabelledByElements } from './roleUtils';
import type { Map } from '@isomorphic/builtins';
import type { AttributeSelectorPart } from '@isomorphic/selectorParser';
export function matchesComponentAttribute(obj: any, attr: AttributeSelectorPart) {

View File

@ -14,37 +14,107 @@
* limitations under the License.
*/
import { builtins } from '@isomorphic/builtins';
import { source } from '@isomorphic/utilityScriptSerializers';
import { parseEvaluationResultValue, serializeAsCallArgument } from '@isomorphic/utilityScriptSerializers';
import type { Builtins } from '@isomorphic/builtins';
import type { SerializedValue } from '@isomorphic/utilityScriptSerializers';
// --- This section should match javascript.ts and generated_injected_builtins.js ---
// This runtime guid is replaced by the actual guid at runtime in all generated sources.
const kRuntimeGuid = '$runtime_guid$';
// This flag is replaced by true/false at runtime in all generated sources.
const kUtilityScriptIsUnderTest = false;
// The name of the global playwright binding, referenced in Node.js.
const kPlaywrightBinding = `__playwright__binding__${kRuntimeGuid}`;
// The name of the global property that stores the UtilityScript instance,
// referenced by generated_injected_builtins.js.
const kUtilityScriptGlobalProperty = `__playwright_utility_script__${kRuntimeGuid}`;
export type Builtins = {
setTimeout: Window['setTimeout'],
clearTimeout: Window['clearTimeout'],
setInterval: Window['setInterval'],
clearInterval: Window['clearInterval'],
requestAnimationFrame: Window['requestAnimationFrame'],
cancelAnimationFrame: Window['cancelAnimationFrame'],
requestIdleCallback: Window['requestIdleCallback'],
cancelIdleCallback: (id: number) => void,
performance: Window['performance'],
// eslint-disable-next-line no-restricted-globals
eval: typeof window['eval'],
// eslint-disable-next-line no-restricted-globals
Intl: typeof window['Intl'],
// eslint-disable-next-line no-restricted-globals
Date: typeof window['Date'],
// eslint-disable-next-line no-restricted-globals
Map: typeof window['Map'],
// eslint-disable-next-line no-restricted-globals
Set: typeof window['Set'],
};
// --- End of the matching section ---
export type BindingPayload = {
name: string;
seq: number;
serializedArgs?: SerializedValue[],
};
type BindingData = {
callbacks: Map<number, { resolve: (value: any) => void, reject: (error: Error) => void }>;
lastSeq: number;
handles: Map<number, any>;
};
export class UtilityScript {
// eslint-disable-next-line no-restricted-globals
readonly global: typeof globalThis;
readonly builtins: Builtins;
readonly isUnderTest: boolean;
private _builtins: Builtins;
private _bindings = new Map<string, BindingData>();
constructor(runtimeGuid: string, isUnderTest: boolean) {
if (isUnderTest) {
// eslint-disable-next-line no-restricted-globals
(globalThis as any).builtins = builtins(runtimeGuid);
}
this._builtins = builtins(runtimeGuid);
const result = source(this._builtins);
this.serializeAsCallArgument = result.serializeAsCallArgument;
this.parseEvaluationResultValue = result.parseEvaluationResultValue;
// eslint-disable-next-line no-restricted-globals
constructor(global: typeof globalThis) {
this.global = global;
this.isUnderTest = kUtilityScriptIsUnderTest;
// UtilityScript is evaluated in every page as an InitScript, and saves builtins
// from the global object, before the page has a chance to temper with them.
//
// Later on, any compiled script replaces global invocations of builtins, e.g. setTimeout,
// with a version exported by generate_injected_builtins.js. That file tries to
// get original builtins saved on the instance of UtilityScript, and falls back
// to the global object just in case something goes wrong with InitScript that creates UtilityScript.
this.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 (this.isUnderTest)
(global as any).builtins = this.builtins;
}
readonly serializeAsCallArgument;
readonly parseEvaluationResultValue;
evaluate(isFunction: boolean | undefined, returnByValue: boolean, expression: string, argCount: number, ...argsAndHandles: any[]) {
const args = argsAndHandles.slice(0, argCount);
const handles = argsAndHandles.slice(argCount);
const parameters = [];
for (let i = 0; i < args.length; i++)
parameters[i] = this.parseEvaluationResultValue(args[i], handles);
parameters[i] = parseEvaluationResultValue(args[i], handles);
let result = this._builtins.eval(expression);
let result = eval(expression);
if (isFunction === true) {
result = result(...parameters);
} else if (isFunction === false) {
@ -61,7 +131,53 @@ export class UtilityScript {
// Special handling of undefined to work-around multi-step returnByValue handling in WebKit.
if (value === undefined)
return undefined;
return this.serializeAsCallArgument(value, (value: any) => ({ fallThrough: value }));
return serializeAsCallArgument(value, (value: any) => ({ fallThrough: value }));
}
addBinding(bindingName: string, needsHandle: boolean) {
const data: BindingData = {
callbacks: new Map(),
lastSeq: 0,
handles: new Map(),
};
this._bindings.set(bindingName, data);
(this.global as any)[bindingName] = (...args: any[]) => {
if (needsHandle && args.slice(1).some(arg => arg !== undefined))
throw new Error(`exposeBindingHandle supports a single argument, ${args.length} received`);
const seq = ++data.lastSeq;
const promise = new Promise((resolve, reject) => data.callbacks.set(seq, { resolve, reject }));
let payload: BindingPayload;
if (needsHandle) {
data.handles.set(seq, args[0]);
payload = { name: bindingName, seq };
} else {
const serializedArgs = [];
for (let i = 0; i < args.length; i++) {
serializedArgs[i] = serializeAsCallArgument(args[i], v => {
return { fallThrough: v };
});
}
payload = { name: bindingName, seq, serializedArgs };
}
(this.global as any)[kPlaywrightBinding](JSON.stringify(payload));
return promise;
};
}
takeBindingHandle(arg: { name: string, seq: number }) {
const handles = this._bindings.get(arg.name)!.handles;
const handle = handles.get(arg.seq);
handles.delete(arg.seq);
return handle;
}
deliverBindingResult(arg: { name: string, seq: number, result?: any, error?: any }) {
const callbacks = this._bindings.get(arg.name)!.callbacks;
if ('error' in arg)
callbacks.get(arg.seq)!.reject(arg.error);
else
callbacks.get(arg.seq)!.resolve(arg.result);
callbacks.delete(arg.seq);
}
private _promiseAwareJsonValueNoThrow(value: any) {
@ -86,3 +202,16 @@ export class UtilityScript {
return safeJson(value);
}
}
// eslint-disable-next-line no-restricted-globals
export function ensureUtilityScript(global?: typeof globalThis): UtilityScript {
// eslint-disable-next-line no-restricted-globals
global = global ?? globalThis;
let utilityScript: UtilityScript = (global as any)[kUtilityScriptGlobalProperty];
if (utilityScript)
return utilityScript;
utilityScript = new UtilityScript(global);
Object.defineProperty(global, kUtilityScriptGlobalProperty, { value: utilityScript, configurable: false, enumerable: false, writable: false });
return utilityScript;
}

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Set } from '@isomorphic/builtins';
import { parseAttributeSelector } from '@isomorphic/selectorParser';
import { isInsideScope } from './domUtils';

View File

@ -14,8 +14,6 @@
* limitations under the License.
*/
import { Map } from '@isomorphic/builtins';
export type WebSocketMessage = string | ArrayBufferLike | Blob | ArrayBufferView;
export type WSData = { data: string, isBase64: boolean };

View File

@ -1,5 +1,6 @@
[*]
../../utils/
../../utils/isomorphic/
../
../isomorphic/
../utils

View File

@ -15,6 +15,7 @@
*/
import { assert } from '../../utils';
import { parseEvaluationResultValue } from '../../utils/isomorphic/utilityScriptSerializers';
import * as js from '../javascript';
import * as dom from '../dom';
import { BidiDeserializer } from './third_party/bidiDeserializer';
@ -97,7 +98,7 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
if (response.type === 'success') {
if (returnByValue)
return js.parseEvaluationResultValue(BidiDeserializer.deserialize(response.result));
return parseEvaluationResultValue(BidiDeserializer.deserialize(response.result));
return createHandle(utilityScript._context, response.result);
}
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));

View File

@ -19,7 +19,8 @@ import { eventsHelper } from '../utils/eventsHelper';
import { BrowserContext } from '../browserContext';
import * as dialog from '../dialog';
import * as dom from '../dom';
import { Page, PageBinding } from '../page';
import { kPlaywrightBinding } from '../javascript';
import { Page } from '../page';
import { BidiExecutionContext, createHandle } from './bidiExecutionContext';
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './bidiInput';
import { BidiNetworkManager } from './bidiNetworkManager';
@ -573,7 +574,7 @@ export class BidiPage implements PageDelegate {
}
export const addMainBindingSource =
`function addMainBinding(callback) { globalThis['${PageBinding.kPlaywrightBinding}'] = callback; }`;
`function addMainBinding(callback) { globalThis['${kPlaywrightBinding}'] = callback; }`;
function toBidiExecutionContext(executionContext: dom.FrameExecutionContext): BidiExecutionContext {
return executionContext.delegate as BidiExecutionContext;

View File

@ -35,7 +35,7 @@ import { Recorder } from './recorder';
import { RecorderApp } from './recorder/recorderApp';
import { Tracing } from './trace/recorder/tracing';
import * as js from './javascript';
import * as storageSource from '../generated/storageScriptSource';
import * as rawStorageSource from '../generated/storageScriptSource';
import type { Artifact } from './artifact';
import type { Browser, BrowserOptions } from './browser';
@ -520,8 +520,8 @@ export abstract class BrowserContext extends SdkObject {
const collectScript = `(() => {
const module = {};
${storageSource.source}
const script = new (module.exports.StorageScript())(${JSON.stringify(js.runtimeGuid)}, ${this._browser.options.name === 'firefox'});
${js.prepareGeneratedScript(rawStorageSource.source)}
const script = new (module.exports.StorageScript())(${this._browser.options.name === 'firefox'});
return script.collect(${indexedDB});
})()`;
@ -618,8 +618,8 @@ export abstract class BrowserContext extends SdkObject {
await frame.goto(metadata, originState.origin);
const restoreScript = `(() => {
const module = {};
${storageSource.source}
const script = new (module.exports.StorageScript())(${JSON.stringify(js.runtimeGuid)}, ${this._browser.options.name === 'firefox'});
${js.prepareGeneratedScript(rawStorageSource.source)}
const script = new (module.exports.StorageScript())(${this._browser.options.name === 'firefox'});
return script.restore(${JSON.stringify(originState)});
})()`;
await frame.evaluateExpression(restoreScript, { world: 'utility' });

View File

@ -18,6 +18,7 @@
import { assert } from '../../utils/isomorphic/assert';
import { getExceptionMessage, releaseObject } from './crProtocolHelper';
import { rewriteErrorMessage } from '../../utils/isomorphic/stackTrace';
import { parseEvaluationResultValue } from '../../utils/isomorphic/utilityScriptSerializers';
import * as js from '../javascript';
import * as dom from '../dom';
import { isSessionClosedError } from '../protocolError';
@ -70,7 +71,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
}).catch(rewriteError);
if (exceptionDetails)
throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails));
return returnByValue ? js.parseEvaluationResultValue(remoteObject.value) : createHandle(utilityScript._context, remoteObject);
return returnByValue ? parseEvaluationResultValue(remoteObject.value) : createHandle(utilityScript._context, remoteObject);
}
async getProperties(object: js.JSHandle): Promise<Map<string, js.JSHandle>> {

View File

@ -26,7 +26,7 @@ import * as dom from '../dom';
import * as frames from '../frames';
import { helper } from '../helper';
import * as network from '../network';
import { PageBinding } from '../page';
import { kPlaywrightBinding } from '../javascript';
import { Page, Worker } from '../page';
import { registry } from '../registry';
import { getAccessibilityTree } from './crAccessibility';
@ -508,7 +508,7 @@ class FrameSession {
this._client.send('Log.enable', {}),
lifecycleEventsEnabled = this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
this._client.send('Runtime.enable', {}),
this._client.send('Runtime.addBinding', { name: PageBinding.kPlaywrightBinding }),
this._client.send('Runtime.addBinding', { name: kPlaywrightBinding }),
this._client.send('Page.addScriptToEvaluateOnNewDocument', {
source: '',
worldName: UTILITY_WORLD_NAME,

View File

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

View File

@ -17,7 +17,8 @@
import { Page } from '../page';
import { Dispatcher, existingDispatcher } from './dispatcher';
import { PageDispatcher } from './pageDispatcher';
import * as webSocketMockSource from '../../generated/webSocketMockSource';
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';
@ -95,7 +96,7 @@ export class WebSocketRouteDispatcher extends Dispatcher<{ guid: string }, chann
await target.addInitScript(`
(() => {
const module = {};
${webSocketMockSource.source}
${prepareGeneratedScript(rawWebSocketMockSource.source)}
(module.exports.inject())(globalThis);
})();
`, kInitScriptName);

View File

@ -18,10 +18,10 @@ import fs from 'fs';
import * as js from './javascript';
import { ProgressController } from './progress';
import { asLocator, isUnderTest } from '../utils';
import { asLocator } from '../utils';
import { prepareFilesForUpload } from './fileUploadUtils';
import { isSessionClosedError } from './protocolError';
import * as injectedScriptSource from '../generated/injectedScriptSource';
import * as rawInjectedScriptSource from '../generated/injectedScriptSource';
import type * as frames from './frames';
import type { ElementState, HitTargetInterceptionResult, InjectedScript, InjectedScriptOptions } from '@injected/injectedScript';
@ -33,7 +33,6 @@ import type * as types from './types';
import type { TimeoutOptions } from '../utils/isomorphic/types';
import type * as channels from '@protocol/channels';
export type InputFilesItems = {
filePayloads?: types.FilePayload[],
localPaths?: string[]
@ -91,19 +90,17 @@ 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(),
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}
${js.prepareGeneratedScript(rawInjectedScriptSource.source)}
return new (module.exports.InjectedScript())(globalThis, ${JSON.stringify(options)});
})();
`;

View File

@ -19,8 +19,9 @@ import { assert } from '../../utils';
import { Browser } from '../browser';
import { BrowserContext, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext';
import { TargetClosedError } from '../errors';
import { kPlaywrightBinding } from '../javascript';
import * as network from '../network';
import { kBuiltinsScript, PageBinding } from '../page';
import { getUtilityInitScript } from '../page';
import { ConnectionEvents, FFConnection } from './ffConnection';
import { FFPage } from './ffPage';
@ -184,7 +185,7 @@ export class FFBrowserContext extends BrowserContext {
const browserContextId = this._browserContextId;
const promises: Promise<any>[] = [
super._initialize(),
this._browser.session.send('Browser.addBinding', { browserContextId: this._browserContextId, name: PageBinding.kPlaywrightBinding, script: '' }),
this._browser.session.send('Browser.addBinding', { browserContextId: this._browserContextId, name: kPlaywrightBinding, script: '' }),
this._updateInitScripts(),
];
if (this._options.acceptDownloads !== 'internal-browser-default') {
@ -378,7 +379,7 @@ export class FFBrowserContext extends BrowserContext {
private async _updateInitScripts() {
const bindingScripts = [...this._pageBindings.values()].map(binding => binding.initScript.source);
const initScripts = this.initScripts.map(script => script.source);
await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: [kBuiltinsScript.source, ...bindingScripts, ...initScripts].map(script => ({ script })) });
await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: [getUtilityInitScript().source, ...bindingScripts, ...initScripts].map(script => ({ script })) });
}
async doUpdateRequestInterception(): Promise<void> {

View File

@ -17,6 +17,7 @@
import { assert } from '../../utils/isomorphic/assert';
import { rewriteErrorMessage } from '../../utils/isomorphic/stackTrace';
import { parseEvaluationResultValue } from '../../utils/isomorphic/utilityScriptSerializers';
import * as js from '../javascript';
import * as dom from '../dom';
import { isSessionClosedError } from '../protocolError';
@ -66,7 +67,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
}).catch(rewriteError);
checkException(payload.exceptionDetails);
if (returnByValue)
return js.parseEvaluationResultValue(payload.result!.value);
return parseEvaluationResultValue(payload.result!.value);
return createHandle(utilityScript._context, payload.result!);
}

View File

@ -15,18 +15,41 @@
*/
import { SdkObject } from './instrumentation';
import * as utilityScriptSource from '../generated/utilityScriptSource';
import * as rawUtilityScriptSource from '../generated/utilityScriptSource';
import { createGuid, isUnderTest } from '../utils';
import { builtins } from '../utils/isomorphic/builtins';
import { source } from '../utils/isomorphic/utilityScriptSerializers';
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.
const preparedSources = new Map<string, string>();
export function prepareGeneratedScript(source: string) {
// We do this lazily to pickup late changes to isUnderTest().
let prepared = preparedSources.get(source);
if (!prepared) {
prepared = source.replaceAll('$runtime_guid$', runtimeGuid).replace('kUtilityScriptIsUnderTest = false', `kUtilityScriptIsUnderTest = ${isUnderTest()}`);
preparedSources.set(source, prepared);
}
return prepared;
}
// 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;
// --- End of the matching section ---
interface TaggedAsJSHandle<T> {
__jshandle: T;
}
@ -49,10 +72,6 @@ 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(runtimeGuid));
export const parseEvaluationResultValue = utilityScriptSerializers.parseEvaluationResultValue;
export const serializeAsCallArgument = utilityScriptSerializers.serializeAsCallArgument;
export interface ExecutionContextDelegate {
rawEvaluateJSON(expression: string): Promise<any>;
rawEvaluateHandle(context: ExecutionContext, expression: string): Promise<JSHandle>;
@ -111,8 +130,8 @@ export class ExecutionContext extends SdkObject {
const source = `
(() => {
const module = {};
${utilityScriptSource.source}
return new (module.exports.UtilityScript())(${JSON.stringify(runtimeGuid)}, ${isUnderTest()});
${prepareGeneratedScript(rawUtilityScriptSource.source)}
return (module.exports.ensureUtilityScript())();
})();`;
this._utilityScriptPromise = this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(this, source))
.then(handle => {

View File

@ -24,8 +24,6 @@ import * as frames from './frames';
import { helper } from './helper';
import * as input from './input';
import { SdkObject } from './instrumentation';
import { builtinsSource } from '../utils/isomorphic/builtins';
import { createPageBindingScript, deliverBindingResult, takeBindingHandle } from './pageBinding';
import * as js from './javascript';
import { ProgressController } from './progress';
import { Screenshotter, validateScreenshotOptions } from './screenshotter';
@ -37,7 +35,9 @@ import { getComparator } from './utils/comparators';
import { debugLogger } from './utils/debugLogger';
import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser';
import { ManualPromise } from '../utils/isomorphic/manualPromise';
import { parseEvaluationResultValue } from '../utils/isomorphic/utilityScriptSerializers';
import { compressCallLog } from './callLog';
import * as rawUtilityScriptSource from '../generated/utilityScriptSource';
import type { Artifact } from './artifact';
import type * as dom from './dom';
@ -49,7 +49,7 @@ import type * as types from './types';
import type { TimeoutOptions } from '../utils/isomorphic/types';
import type { ImageComparatorOptions } from './utils/comparators';
import type * as channels from '@protocol/channels';
import type { BindingPayload } from './pageBinding';
import type { BindingPayload } from '@injected/utilityScript';
export interface PageDelegate {
readonly rawMouse: input.RawMouse;
@ -772,7 +772,7 @@ export class Page extends SdkObject {
allInitScripts() {
const bindings = [...this._browserContext._pageBindings.values(), ...this._pageBindings.values()];
return [kBuiltinsScript, ...bindings.map(binding => binding.initScript), ...this._browserContext.initScripts, ...this.initScripts];
return [getUtilityInitScript(), ...bindings.map(binding => binding.initScript), ...this._browserContext.initScripts, ...this.initScripts];
}
getBinding(name: string) {
@ -858,7 +858,6 @@ export class Worker extends SdkObject {
}
export class PageBinding {
static kPlaywrightBinding = '__playwright__binding__' + js.runtimeGuid;
readonly name: string;
readonly playwrightFunction: frames.FunctionWithSource;
@ -869,7 +868,7 @@ export class PageBinding {
constructor(name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean) {
this.name = name;
this.playwrightFunction = playwrightFunction;
this.initScript = new InitScript(createPageBindingScript(PageBinding.kPlaywrightBinding, name, needsHandle), true /* internal */);
this.initScript = new InitScript(`${js.accessUtilityScript()}.addBinding(${JSON.stringify(name)}, ${needsHandle})`, true /* internal */);
this.needsHandle = needsHandle;
this.internal = name.startsWith('__pw');
}
@ -883,17 +882,17 @@ export class PageBinding {
throw new Error(`Function "${name}" is not exposed`);
let result: any;
if (binding.needsHandle) {
const handle = await context.evaluateHandle(takeBindingHandle, { name, seq }).catch(e => null);
const handle = await context.evaluateExpressionHandle(`arg => ${js.accessUtilityScript()}.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))
throw new Error(`serializedArgs is not an array. This can happen when Array.prototype.toJSON is defined incorrectly`);
const args = serializedArgs!.map(a => js.parseEvaluationResultValue(a));
const args = serializedArgs!.map(a => parseEvaluationResultValue(a));
result = await binding.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, ...args);
}
context.evaluate(deliverBindingResult, { name, seq, result }).catch(e => debugLogger.log('error', e));
context.evaluateExpressionHandle(`arg => ${js.accessUtilityScript()}.deliverBindingResult(arg)`, { isFunction: true }, { name, seq, result }).catch(e => debugLogger.log('error', e));
} catch (error) {
context.evaluate(deliverBindingResult, { name, seq, error }).catch(e => debugLogger.log('error', e));
context.evaluateExpressionHandle(`arg => ${js.accessUtilityScript()}.deliverBindingResult(arg)`, { isFunction: true }, { name, seq, error }).catch(e => debugLogger.log('error', e));
}
}
}
@ -922,7 +921,19 @@ export class InitScript {
}
}
export const kBuiltinsScript = new InitScript(builtinsSource(js.runtimeGuid), true /* internal */);
let utilityInitScriptInstance: InitScript | undefined;
export function getUtilityInitScript() {
if (!utilityInitScriptInstance) {
utilityInitScriptInstance = new InitScript(`
(() => {
const module = {};
${js.prepareGeneratedScript(rawUtilityScriptSource.source)}
(module.exports.ensureUtilityScript())();
})();
`, true /* internal */);
}
return utilityInitScriptInstance;
}
class FrameThrottler {
private _acks: (() => void)[] = [];

View File

@ -1,93 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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';
export type BindingPayload = {
name: string;
seq: number;
serializedArgs?: SerializedValue[],
};
function addPageBinding(playwrightBinding: string, bindingName: string, needsHandle: boolean, utilityScriptSerializersFactory: typeof source, builtins: Builtins) {
const { serializeAsCallArgument } = utilityScriptSerializersFactory(builtins);
// eslint-disable-next-line no-restricted-globals
const binding = (globalThis as any)[playwrightBinding];
// eslint-disable-next-line no-restricted-globals
(globalThis as any)[bindingName] = (...args: any[]) => {
// eslint-disable-next-line no-restricted-globals
const me = (globalThis as any)[bindingName];
if (needsHandle && args.slice(1).some(arg => arg !== undefined))
throw new Error(`exposeBindingHandle supports a single argument, ${args.length} received`);
let callbacks = me['callbacks'];
if (!callbacks) {
callbacks = new builtins.Map();
me['callbacks'] = callbacks;
}
const seq: number = (me['lastSeq'] || 0) + 1;
me['lastSeq'] = seq;
let handles = me['handles'];
if (!handles) {
handles = new builtins.Map();
me['handles'] = handles;
}
const promise = new Promise((resolve, reject) => callbacks.set(seq, { resolve, reject }));
let payload: BindingPayload;
if (needsHandle) {
handles.set(seq, args[0]);
payload = { name: bindingName, seq };
} else {
const serializedArgs = [];
for (let i = 0; i < args.length; i++) {
serializedArgs[i] = serializeAsCallArgument(args[i], v => {
return { fallThrough: v };
});
}
payload = { name: bindingName, seq, serializedArgs };
}
binding(JSON.stringify(payload));
return promise;
};
// eslint-disable-next-line no-restricted-globals
(globalThis as any)[bindingName].__installed = true;
}
export function takeBindingHandle(arg: { name: string, seq: number }) {
// eslint-disable-next-line no-restricted-globals
const handles = (globalThis as any)[arg.name]['handles'];
const handle = handles.get(arg.seq);
handles.delete(arg.seq);
return handle;
}
export function deliverBindingResult(arg: { name: string, seq: number, result?: any, error?: any }) {
// eslint-disable-next-line no-restricted-globals
const callbacks = (globalThis as any)[arg.name]['callbacks'];
if ('error' in arg)
callbacks.get(arg.seq).reject(arg.error);
else
callbacks.get(arg.seq).resolve(arg.result);
callbacks.delete(arg.seq);
}
export function createPageBindingScript(playwrightBinding: string, name: string, needsHandle: boolean) {
return `(${addPageBinding.toString()})(${JSON.stringify(playwrightBinding)}, ${JSON.stringify(name)}, ${needsHandle}, (${source}), ${builtinsSource(js.runtimeGuid)})`;
}

View File

@ -17,7 +17,7 @@
import { EventEmitter } from 'events';
import { RecorderCollection } from './recorderCollection';
import * as recorderSource from '../../generated/pollingRecorderSource';
import * as rawRecorderSource from '../../generated/pollingRecorderSource';
import { eventsHelper, monotonicTime, quoteCSSAttributeValue } from '../../utils';
import { raceAgainstDeadline } from '../../utils/isomorphic/timeoutRunner';
import { BrowserContext } from '../browserContext';
@ -26,6 +26,7 @@ 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';
@ -146,7 +147,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(recorderSource.source);
await this._context.extendInjectedScript(prepareGeneratedScript(rawRecorderSource.source));
}
setEnabled(enabled: boolean) {

View File

@ -14,29 +14,20 @@
* limitations under the License.
*/
import { builtins } from '@isomorphic/builtins';
import { source } from '@isomorphic/utilityScriptSerializers';
import { parseEvaluationResultValue, serializeAsCallArgument } from '../utils/isomorphic/utilityScriptSerializers';
import type { Builtins } from '@isomorphic/builtins';
import type * as channels from '@protocol/channels';
export type SerializedStorage = Omit<channels.OriginStorage, 'origin'>;
export class StorageScript {
private _builtins: Builtins;
private _isFirefox: boolean;
private _global;
private _serializeAsCallArgument;
private _parseEvaluationResultValue;
constructor(runtimeGuid: string, isFirefox: boolean) {
this._builtins = builtins(runtimeGuid);
constructor(isFirefox: boolean) {
this._isFirefox = isFirefox;
// eslint-disable-next-line no-restricted-globals
this._global = globalThis;
const result = source(this._builtins);
this._serializeAsCallArgument = result.serializeAsCallArgument;
this._parseEvaluationResultValue = result.parseEvaluationResultValue;
}
private _idbRequestToPromise<T extends IDBOpenDBRequest | IDBRequest>(request: T) {
@ -58,7 +49,7 @@ export class StorageScript {
private _trySerialize(value: any): { trivial?: any, encoded?: any } {
let trivial = true;
const encoded = this._serializeAsCallArgument(value, v => {
const encoded = serializeAsCallArgument(value, v => {
const isTrivial = (
this._isPlainObject(v)
|| Array.isArray(v)
@ -177,8 +168,8 @@ export class StorageScript {
await Promise.all(store.records.map(async record => {
await this._idbRequestToPromise(
objectStore.add(
record.value ?? this._parseEvaluationResultValue(record.valueEncoded),
record.key ?? this._parseEvaluationResultValue(record.keyEncoded),
record.value ?? parseEvaluationResultValue(record.valueEncoded),
record.key ?? parseEvaluationResultValue(record.keyEncoded),
)
);
}));

View File

@ -19,6 +19,7 @@ import * as js from '../javascript';
import * as dom from '../dom';
import { isSessionClosedError } from '../protocolError';
import { assert } from '../../utils/isomorphic/assert';
import { parseEvaluationResultValue } from '../../utils/isomorphic/utilityScriptSerializers';
import type { Protocol } from './protocol';
import type { WKSession } from './wkConnection';
@ -79,7 +80,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
if (response.wasThrown)
throw new js.JavaScriptErrorInEvaluate(response.result.description);
if (returnByValue)
return js.parseEvaluationResultValue(response.result.value);
return parseEvaluationResultValue(response.result.value);
return createHandle(utilityScript._context, response.result);
} catch (error) {
throw rewriteError(error);

View File

@ -30,7 +30,7 @@ import * as dom from '../dom';
import { TargetClosedError } from '../errors';
import { helper } from '../helper';
import * as network from '../network';
import { PageBinding } from '../page';
import { kPlaywrightBinding } from '../javascript';
import { Page } from '../page';
import { getAccessibilityTree } from './wkAccessibility';
import { WKSession } from './wkConnection';
@ -176,7 +176,7 @@ export class WKPage implements PageDelegate {
const promises: Promise<any>[] = [
// Resource tree should be received before first execution context.
session.send('Runtime.enable'),
session.send('Runtime.addBinding', { name: PageBinding.kPlaywrightBinding }),
session.send('Runtime.addBinding', { name: kPlaywrightBinding }),
session.send('Page.createUserWorld', { name: UTILITY_WORLD_NAME }).catch(_ => {}), // Worlds are per-process
session.send('Console.enable'),
session.send('Network.enable'),

View File

@ -1,89 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable no-restricted-globals */
// Make sure to update eslint.config.mjs when changing the list of builitins.
export type Builtins = {
setTimeout: Window['setTimeout'],
clearTimeout: Window['clearTimeout'],
setInterval: Window['setInterval'],
clearInterval: Window['clearInterval'],
requestAnimationFrame: Window['requestAnimationFrame'],
cancelAnimationFrame: Window['cancelAnimationFrame'],
requestIdleCallback: Window['requestIdleCallback'],
cancelIdleCallback: (id: number) => void,
performance: Window['performance'],
eval: typeof window['eval'],
Intl: typeof window['Intl'],
Date: typeof window['Date'],
Map: typeof window['Map'],
Set: typeof window['Set'],
};
// Builtins are created once lazily upon the first import of this module, see "instance" below.
// This is how it works in Node.js environment, or when something goes unexpectedly in the browser.
//
// However, the same "builtins()" function is also evaluated inside an InitScript before
// 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(runtimeGuid: string, global?: typeof globalThis): Builtins {
global = global ?? globalThis;
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;
}
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;
export const clearInterval = instance.clearInterval;
export const requestAnimationFrame = instance.requestAnimationFrame;
export const cancelAnimationFrame = instance.cancelAnimationFrame;
export const requestIdleCallback = instance.requestIdleCallback;
export const cancelIdleCallback = instance.cancelIdleCallback;
export const performance = instance.performance;
export const Intl = instance.Intl;
export const Date = instance.Date;
export const Map = instance.Map;
export const Set = instance.Set;

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Set } from './builtins';
import * as css from './cssTokenizer';
export class InvalidSelectorError extends Error {

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Map } from './builtins';
import { captureRawStack } from './stackTrace';
export class ManualPromise<T = void> extends Promise<T> {

View File

@ -14,8 +14,6 @@
* limitations under the License.
*/
import { Map } from './builtins';
export function isJsonMimeType(mimeType: string) {
return !!mimeType.match(/^(application\/json|application\/.*?\+json|text\/(x-)?json)(;\s*charset=.*)?$/);
}

View File

@ -14,8 +14,6 @@
* limitations under the License.
*/
import { Map } from './builtins';
export class MultiMap<K, V> {
private _map: Map<K, V[]>;

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Set } from './builtins';
import { InvalidSelectorError, parseCSS } from './cssParser';
import type { CSSComplexSelectorList } from './cssParser';

View File

@ -14,8 +14,6 @@
* limitations under the License.
*/
import { Map } from './builtins';
// NOTE: this function should not be used to escape any selectors.
export function escapeWithQuotes(text: string, char: string = '\'') {
const stringified = JSON.stringify(text);

View File

@ -14,8 +14,6 @@
* limitations under the License.
*/
import { performance } from './builtins';
let _timeOrigin = performance.timeOrigin;
let _timeShift = 0;

View File

@ -14,11 +14,10 @@
* limitations under the License.
*/
import { setTimeout, clearTimeout } from './builtins';
import { monotonicTime } from './time';
export async function raceAgainstDeadline<T>(cb: () => Promise<T>, deadline: number): Promise<{ result: T, timedOut: false } | { timedOut: true }> {
let timer: number | undefined;
let timer: NodeJS.Timeout | undefined;
return Promise.race([
cb().then(result => {
return { result, timedOut: false };

View File

@ -14,8 +14,6 @@
* limitations under the License.
*/
import { Map } from './builtins';
import type { ClientSideCallMetadata, StackFrame } from '@protocol/channels';
export type SerializedStackFrame = [number, number, number, string];

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Map, Set } from './builtins';
import { isString } from './stringUtils';
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping

View File

@ -14,8 +14,6 @@
* limitations under the License.
*/
import type { Date, Map, Builtins } from './builtins';
type TypedArrayKind = 'i8' | 'ui8' | 'ui8c' | 'i16' | 'ui16' | 'i32' | 'ui32' | 'f32' | 'f64' | 'bi64' | 'bui64';
export type SerializedValue =
@ -39,7 +37,7 @@ type VisitorInfo = {
lastId: number;
};
export function source(builtins: Builtins) {
function source() {
function isRegExp(obj: any): obj is RegExp {
try {
@ -51,7 +49,7 @@ export function source(builtins: Builtins) {
function isDate(obj: any): obj is Date {
try {
return obj instanceof builtins.Date || Object.prototype.toString.call(obj) === '[object Date]';
return obj instanceof Date || Object.prototype.toString.call(obj) === '[object Date]';
} catch (error) {
return false;
}
@ -115,7 +113,7 @@ export function source(builtins: Builtins) {
return new TypedArrayConstructor(bytes.buffer);
}
function parseEvaluationResultValue(value: SerializedValue, handles: any[] = [], refs: Map<number, object> = new builtins.Map()): any {
function parseEvaluationResultValue(value: SerializedValue, handles: any[] = [], refs: Map<number, object> = new Map()): any {
if (Object.is(value, undefined))
return undefined;
if (typeof value === 'object' && value) {
@ -137,7 +135,7 @@ export function source(builtins: Builtins) {
return undefined;
}
if ('d' in value)
return new builtins.Date(value.d);
return new Date(value.d);
if ('u' in value)
return new URL(value.u);
if ('bi' in value)
@ -173,7 +171,7 @@ export function source(builtins: Builtins) {
}
function serializeAsCallArgument(value: any, handleSerializer: (value: any) => HandleOrValue): SerializedValue {
return serialize(value, handleSerializer, { visited: new builtins.Map(), lastId: 0 });
return serialize(value, handleSerializer, { visited: new Map(), lastId: 0 });
}
function serialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue {
@ -289,3 +287,6 @@ export function source(builtins: Builtins) {
return { parseEvaluationResultValue, serializeAsCallArgument };
}
const { parseEvaluationResultValue, serializeAsCallArgument } = source();
export { parseEvaluationResultValue, serializeAsCallArgument };

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={() => {
const win = window.open(snapshotUrls?.popoutUrl || '', '_blank');
win?.addEventListener('DOMContentLoaded', () => {
const injectedScript = new InjectedScript(win as any, { isUnderTest: false, sdkLanguage, testIdAttributeName, stableRafCount: 1, browserName: 'chromium', inputFileRoleTextbox: false, customEngines: [], runtimeGuid: '' });
const injectedScript = new InjectedScript(win as any, { isUnderTest, sdkLanguage, testIdAttributeName, stableRafCount: 1, browserName: 'chromium', inputFileRoleTextbox: false, customEngines: [] });
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, stableRafCount: 1, browserName: 'chromium', inputFileRoleTextbox: false, customEngines: [], runtimeGuid: '' });
const injectedScript = new InjectedScript(frameWindow as any, { isUnderTest, sdkLanguage, testIdAttributeName, stableRafCount: 1, browserName: 'chromium', inputFileRoleTextbox: false, customEngines: [] });
const recorder = new Recorder(injectedScript);
win._injectedScript = injectedScript;
win._recorder = { recorder, frameSelector: parentFrameSelector };

View File

@ -22,7 +22,7 @@ import type { ServerFixtures, ServerWorkerOptions } from './serverFixtures';
import { serverFixtures } from './serverFixtures';
import { platformTest } from './platformFixtures';
import { testModeTest } from './testModeFixtures';
import type { Builtins } from '../../packages/playwright-core/src/utils/isomorphic/builtins';
import type { Builtins } from '../../packages/injected/src/utilityScript';
export const base = test;

View File

@ -84,7 +84,7 @@ it('should work with bogus Array.from', async ({ page, server }) => {
expect(divsCount).toBe(3);
});
it.fixme('should work with broken Map', async ({ page, server }) => {
it('should work with broken Map', async ({ page, server }) => {
await page.setContent(`
<script>
window.Map = () => {};

View File

@ -376,6 +376,7 @@ onChanges.push({
'packages/playwright-ct-core/src/injected/**',
'packages/playwright-core/src/utils/isomorphic/**',
'packages/playwright-core/src/server/storageScript.ts',
'utils/generate_injected_builtins.js',
'utils/generate_injected.js',
],
script: 'utils/generate_injected.js',

View File

@ -130,6 +130,7 @@ const inlineCSSPlugin = {
platform: 'browser',
target: 'ES2019',
plugins: [inlineCSSPlugin],
inject: hasExports ? [require.resolve('./generate_injected_builtins.js')] : [],
});
for (const message of [...buildOutput.errors, ...buildOutput.warnings])
console.log(message.text);
@ -138,6 +139,9 @@ 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);
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// IMPORTANT: This file should match javascript.ts and utilityScript.ts
const gSetTimeout = globalThis.__playwright_utility_script__$runtime_guid$?.builtins.setTimeout ?? globalThis.setTimeout;
const gClearTimeout = globalThis.__playwright_utility_script__$runtime_guid$?.builtins.clearTimeout ?? globalThis.clearTimeout;
const gSetInterval = globalThis.__playwright_utility_script__$runtime_guid$?.builtins.setInterval ?? globalThis.setInterval;
const gClearInterval = globalThis.__playwright_utility_script__$runtime_guid$?.builtins.clearInterval ?? globalThis.clearInterval;
const gRequestAnimationFrame = globalThis.__playwright_utility_script__$runtime_guid$?.builtins.requestAnimationFrame ?? globalThis.requestAnimationFrame;
const gCancelAnimationFrame = globalThis.__playwright_utility_script__$runtime_guid$?.builtins.cancelAnimationFrame ?? globalThis.cancelAnimationFrame;
const gRequestIdleCallback = globalThis.__playwright_utility_script__$runtime_guid$?.builtins.requestIdleCallback ?? globalThis.requestIdleCallback;
const gCancelIdleCallback = globalThis.__playwright_utility_script__$runtime_guid$?.builtins.cancelIdleCallback ?? globalThis.cancelIdleCallback;
const gPerformance = globalThis.__playwright_utility_script__$runtime_guid$?.builtins.performance ?? globalThis.performance;
const gEval = globalThis.__playwright_utility_script__$runtime_guid$?.builtins.eval ?? globalThis.eval;
const gIntl = globalThis.__playwright_utility_script__$runtime_guid$?.builtins.Intl ?? globalThis.Intl;
const gDate = globalThis.__playwright_utility_script__$runtime_guid$?.builtins.Date ?? globalThis.Date;
const gMap = globalThis.__playwright_utility_script__$runtime_guid$?.builtins.Map ?? globalThis.Map;
const gSet = globalThis.__playwright_utility_script__$runtime_guid$?.builtins.Set ?? globalThis.Set;
export {
gSetTimeout as 'setTimeout',
gClearTimeout as 'clearTimeout',
gSetInterval as 'setInterval',
gClearInterval as 'clearInterval',
gRequestAnimationFrame as 'requestAnimationFrame',
gCancelAnimationFrame as 'cancelAnimationFrame',
gRequestIdleCallback as 'requestIdleCallback',
gCancelIdleCallback as 'cancelIdleCallback',
gPerformance as 'performance',
gEval as 'eval',
gIntl as 'Intl',
gDate as 'Date',
gMap as 'Map',
gSet as 'Set',
};