chore: evaluate UtilityScript lazily (#36019)

This commit is contained in:
Dmitry Gozman 2025-05-21 13:45:50 +00:00 committed by GitHub
parent 92d4ce30c6
commit 6af41232b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 110 additions and 253 deletions

View File

@ -219,9 +219,21 @@ const noBooleanCompareRules = {
};
const noWebGlobalsRuleList = [
// Keep in sync with builtins from utilityScript.ts
{ name: "window", message: "Use InjectedScript.window instead" },
{ name: "document", message: "Use InjectedScript.document instead" },
{ name: "globalThis", message: "Use InjectedScript.window instead" },
{ name: "setTimeout", message: "Use InjectedScript.utils.builtins.setTimeout instead" },
{ name: "clearTimeout", message: "Use InjectedScript.utils.builtins.clearTimeout instead" },
{ name: "setInterval", message: "Use InjectedScript.utils.builtins.setInterval instead" },
{ name: "clearInterval", message: "Use InjectedScript.utils.builtins.clearInterval instead" },
{ name: "requestAnimationFrame", message: "Use InjectedScript.utils.builtins.requestAnimationFrame instead" },
{ name: "cancelAnimationFrame", message: "Use InjectedScript.utils.builtins.cancelAnimationFrame instead" },
{ name: "requestIdleCallback", message: "Use InjectedScript.utils.builtins.requestIdleCallback instead" },
{ name: "cancelIdleCallback", message: "Use InjectedScript.utils.builtins.cancelIdleCallback instead" },
{ name: "Date", message: "Use InjectedScript.utils.builtins.Date instead" },
{ name: "Intl", message: "Use InjectedScript.utils.builtins.Intl instead" },
{ name: "performance", message: "Use InjectedScript.utils.builtins.performance instead" },
];
const noNodeGlobalsRuleList = [{ name: "process" }];

View File

@ -10,26 +10,14 @@
* 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.
*/
export type ClockMethods = {
Date: DateConstructor;
setTimeout: Window['setTimeout'];
clearTimeout: Window['clearTimeout'];
setInterval: Window['setInterval'];
clearInterval: Window['clearInterval'];
requestAnimationFrame?: Window['requestAnimationFrame'];
cancelAnimationFrame?: (id: number) => void;
requestIdleCallback?: Window['requestIdleCallback'];
cancelIdleCallback?: (id: number) => void;
Intl?: typeof Intl;
performance?: Window['performance'];
};
import type { Builtins } from './utilityScript';
export type ClockConfig = {
now?: number | Date;
now?: number;
};
export type InstallConfig = ClockConfig & {
toFake?: (keyof ClockMethods)[];
toFake?: (keyof Builtins)[];
};
enum TimerType {
@ -427,7 +415,7 @@ export class ClockController {
}
}
function mirrorDateProperties(target: any, source: DateConstructor): DateConstructor & Date {
function mirrorDateProperties(target: any, source: Builtins['Date']): Builtins['Date'] {
for (const prop in source) {
if (source.hasOwnProperty(prop))
target[prop] = (source as any)[prop];
@ -441,7 +429,8 @@ function mirrorDateProperties(target: any, source: DateConstructor): DateConstru
return target;
}
function createDate(clock: ClockController, NativeDate: DateConstructor): DateConstructor & Date {
function createDate(clock: ClockController, NativeDate: Builtins['Date']): Builtins['Date'] {
// eslint-disable-next-line no-restricted-globals
function ClockDate(this: typeof ClockDate, year: number, month: number, date: number, hour: number, minute: number, second: number, ms: number): Date | string {
// the Date constructor called as a function, ref Ecma-262 Edition 5.1, section 15.9.2.
// This remains so in the 10th edition of 2019 as well.
@ -497,17 +486,18 @@ function createDate(clock: ClockController, NativeDate: DateConstructor): DateCo
* but we need to take control of those that have a
* dependency on the current clock.
*/
function createIntl(clock: ClockController, NativeIntl: typeof Intl): typeof Intl {
function createIntl(clock: ClockController, NativeIntl: Builtins['Intl']): Builtins['Intl'] {
const ClockIntl: any = {};
/*
* All properties of Intl are non-enumerable, so we need
* to do a bit of work to get them out.
*/
for (const key of Object.getOwnPropertyNames(NativeIntl) as (keyof typeof Intl)[])
for (const key of Object.getOwnPropertyNames(NativeIntl) as (keyof Builtins['Intl'])[])
ClockIntl[key] = NativeIntl[key];
ClockIntl.DateTimeFormat = function(...args: any[]) {
const realFormatter = new NativeIntl.DateTimeFormat(...args);
// eslint-disable-next-line no-restricted-globals
const formatter: Intl.DateTimeFormat = {
formatRange: realFormatter.formatRange.bind(realFormatter),
formatRangeToParts: realFormatter.formatRangeToParts.bind(realFormatter),
@ -560,8 +550,8 @@ function compareTimers(a: Timer, b: Timer) {
const maxTimeout = Math.pow(2, 31) - 1; // see https://heycam.github.io/webidl/#abstract-opdef-converttoint
const idCounterStart = 1e12; // arbitrarily large number to avoid collisions with native timer IDs
function platformOriginals(globalObject: WindowOrWorkerGlobalScope): { raw: ClockMethods, bound: ClockMethods } {
const raw: ClockMethods = {
function platformOriginals(globalObject: WindowOrWorkerGlobalScope): { raw: Builtins, bound: Builtins } {
const raw: Builtins = {
setTimeout: globalObject.setTimeout,
clearTimeout: globalObject.clearTimeout,
setInterval: globalObject.setInterval,
@ -575,7 +565,7 @@ function platformOriginals(globalObject: WindowOrWorkerGlobalScope): { raw: Cloc
Intl: (globalObject as any).Intl,
};
const bound = { ...raw };
for (const key of Object.keys(bound) as (keyof ClockMethods)[]) {
for (const key of Object.keys(bound) as (keyof Builtins)[]) {
if (key !== 'Date' && typeof bound[key] === 'function')
bound[key] = (bound[key] as any).bind(globalObject);
}
@ -592,7 +582,7 @@ function getScheduleHandler(type: TimerType) {
return `set${type}`;
}
function createApi(clock: ClockController, originals: ClockMethods): ClockMethods {
function createApi(clock: ClockController, originals: Builtins): Builtins {
return {
setTimeout: (func: TimerHandler, timeout?: number | undefined, ...args: any[]) => {
const delay = timeout ? +timeout : timeout;
@ -646,9 +636,9 @@ function createApi(clock: ClockController, originals: ClockMethods): ClockMethod
if (timerId)
return clock.clearTimer(timerId, TimerType.IdleCallback);
},
Intl: originals.Intl ? createIntl(clock, originals.Intl) : undefined,
Intl: originals.Intl ? createIntl(clock, originals.Intl) : (undefined as unknown as Builtins['Intl']),
Date: createDate(clock, originals.Date),
performance: originals.performance ? fakePerformance(clock, originals.performance) : undefined,
performance: originals.performance ? fakePerformance(clock, originals.performance) : (undefined as unknown as Builtins['performance']),
};
}
@ -659,7 +649,7 @@ function getClearHandler(type: TimerType) {
return `clear${type}`;
}
function fakePerformance(clock: ClockController, performance: Performance): Performance {
function fakePerformance(clock: ClockController, performance: Builtins['performance']): Builtins['performance'] {
const result: any = {
now: () => clock.performanceNow(),
};
@ -676,7 +666,7 @@ function fakePerformance(clock: ClockController, performance: Performance): Perf
return result;
}
export function createClock(globalObject: WindowOrWorkerGlobalScope): { clock: ClockController, api: ClockMethods, originals: ClockMethods } {
export function createClock(globalObject: WindowOrWorkerGlobalScope): { clock: ClockController, api: Builtins, originals: Builtins } {
const originals = platformOriginals(globalObject);
const embedder: Embedder = {
dateNow: () => originals.raw.Date.now(),
@ -696,7 +686,7 @@ export function createClock(globalObject: WindowOrWorkerGlobalScope): { clock: C
return { clock, api, originals: originals.raw };
}
export function install(globalObject: WindowOrWorkerGlobalScope, config: InstallConfig = {}): { clock: ClockController, api: ClockMethods, originals: ClockMethods } {
export function install(globalObject: WindowOrWorkerGlobalScope, config: InstallConfig = {}): { clock: ClockController, api: Builtins, originals: Builtins } {
if ((globalObject as any).Date?.isFake) {
// Timers are already faked; this is a problem.
// Make the user reset timers before continuing.
@ -704,7 +694,7 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install
}
const { clock, api, originals } = createClock(globalObject);
const toFake = config.toFake?.length ? config.toFake : Object.keys(originals) as (keyof ClockMethods)[];
const toFake = config.toFake?.length ? config.toFake : Object.keys(originals) as (keyof Builtins)[];
for (const method of toFake) {
if (method === 'Date') {
@ -735,12 +725,12 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install
}
export function inject(globalObject: WindowOrWorkerGlobalScope) {
const builtin = platformOriginals(globalObject).bound;
const builtins = platformOriginals(globalObject).bound;
const { clock: controller } = install(globalObject);
controller.resume();
return {
controller,
builtin,
builtins,
};
}

View File

@ -100,7 +100,7 @@ export class Highlight {
runHighlightOnRaf(selector: ParsedSelector) {
if (this._rafRequest)
cancelAnimationFrame(this._rafRequest);
this._injectedScript.utils.builtins.cancelAnimationFrame(this._rafRequest);
const elements = this._injectedScript.querySelectorAll(selector, this._injectedScript.document.documentElement);
const locator = asLocator(this._language, stringifySelector(selector));
const color = elements.length > 1 ? '#f6b26b7f' : '#6fa8dc7f';
@ -108,12 +108,12 @@ export class Highlight {
const suffix = elements.length > 1 ? ` [${index + 1} of ${elements.length}]` : '';
return { element, color, tooltipText: locator + suffix };
}));
this._rafRequest = requestAnimationFrame(() => this.runHighlightOnRaf(selector));
this._rafRequest = this._injectedScript.utils.builtins.requestAnimationFrame(() => this.runHighlightOnRaf(selector));
}
uninstall() {
if (this._rafRequest)
cancelAnimationFrame(this._rafRequest);
this._injectedScript.utils.builtins.cancelAnimationFrame(this._rafRequest);
this._glassPaneElement.remove();
}

View File

@ -32,7 +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 { UtilityScript } from './utilityScript';
import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot';
import type { CSSComplexSelectorList } from '@isomorphic/cssParser';
@ -108,6 +108,7 @@ export class InjectedScript {
isInsideScope,
normalizeWhiteSpace,
parseAriaSnapshot,
// Builtins protect injected code from clock emulation.
builtins: null as unknown as Builtins,
};
@ -125,7 +126,7 @@ export class InjectedScript {
this.document = window.document;
// Make sure builtins are created from "window". This is important for InjectedScript instantiated
// inside a trace viewer snapshot, where "window" differs from "globalThis".
const utilityScript = ensureUtilityScript(window);
const utilityScript = new UtilityScript(window);
this.isUnderTest = options.isUnderTest ?? utilityScript.isUnderTest;
this.utils.builtins = utilityScript.builtins;
this._sdkLanguage = options.sdkLanguage;
@ -564,7 +565,7 @@ export class InjectedScript {
observer.observe(element);
// Firefox doesn't call IntersectionObserver callback unless
// there are rafs.
requestAnimationFrame(() => {});
this.utils.builtins.requestAnimationFrame(() => {});
});
}
@ -645,7 +646,7 @@ export class InjectedScript {
return 'error:notconnected';
// Drop frames that are shorter than 16ms - WebKit Win bug.
const time = performance.now();
const time = this.utils.builtins.performance.now();
if (this._stableRafCount > 1 && time - lastTime < 15)
return continuePolling;
lastTime = time;
@ -673,12 +674,12 @@ export class InjectedScript {
if (success !== continuePolling)
fulfill(success);
else
requestAnimationFrame(raf);
this.utils.builtins.requestAnimationFrame(raf);
} catch (e) {
reject(e);
}
};
requestAnimationFrame(raf);
this.utils.builtins.requestAnimationFrame(raf);
return result;
}

View File

@ -197,7 +197,7 @@ class RecordActionTool implements RecorderTool {
constructor(recorder: Recorder) {
this._recorder = recorder;
this._performingActions = new recorder.injectedScript.utils.builtins.Set();
this._performingActions = new Set();
}
cursor() {
@ -603,7 +603,7 @@ class TextAssertionTool implements RecorderTool {
constructor(recorder: Recorder, kind: 'text' | 'value' | 'snapshot') {
this._recorder = recorder;
this._textCache = new recorder.injectedScript.utils.builtins.Map();
this._textCache = new Map();
this._kind = kind;
this._dialog = new Dialog(recorder);
}

View File

@ -18,15 +18,10 @@ import { parseEvaluationResultValue, serializeAsCallArgument } from '@isomorphic
// --- 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 property that stores the UtilityScript instance,
// referenced by generated_injected_builtins.js.
const kUtilityScriptGlobalProperty = `__playwright_utility_script__${kRuntimeGuid}`;
// Keep in sync with eslint.config.mjs
export type Builtins = {
setTimeout: Window['setTimeout'],
clearTimeout: Window['clearTimeout'],
@ -35,18 +30,12 @@ export type Builtins = {
requestAnimationFrame: Window['requestAnimationFrame'],
cancelAnimationFrame: Window['cancelAnimationFrame'],
requestIdleCallback: Window['requestIdleCallback'],
cancelIdleCallback: (id: number) => void,
cancelIdleCallback: Window['cancelIdleCallback'],
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 ---
@ -54,6 +43,7 @@ export type Builtins = {
export class UtilityScript {
// eslint-disable-next-line no-restricted-globals
readonly global: typeof globalThis;
// Builtins protect injected code from clock emulation.
readonly builtins: Builtins;
readonly isUnderTest: boolean;
@ -61,29 +51,23 @@ export class UtilityScript {
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 ((global as any).__pwClock) {
this.builtins = (global as any).__pwClock.builtins;
} else {
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,
Intl: global.Intl,
Date: global.Date,
} satisfies Builtins;
}
if (this.isUnderTest)
(global as any).builtins = this.builtins;
}
@ -95,7 +79,7 @@ export class UtilityScript {
for (let i = 0; i < args.length; i++)
parameters[i] = parseEvaluationResultValue(args[i], handles);
let result = eval(expression);
let result = this.global.eval(expression);
if (isFunction === true) {
result = result(...parameters);
} else if (isFunction === false) {
@ -137,16 +121,3 @@ 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

@ -21,7 +21,6 @@ import * as network from '../network';
import { BidiConnection } from './bidiConnection';
import { bidiBytesValueToString } from './bidiNetworkManager';
import { BidiPage, kPlaywrightBindingChannel } from './bidiPage';
import { kUtilityInitScript } from '../page';
import { kPlaywrightBinding } from '../javascript';
import * as bidi from './third_party/bidiProtocol';
@ -222,7 +221,6 @@ export class BidiBrowserContext extends BrowserContext {
override async _initialize() {
const promises: Promise<any>[] = [
super._initialize(),
this._installUtilityScript(),
];
if (this._options.viewport) {
promises.push(this._browser._browserSession.send('browsingContext.setViewport', {
@ -239,13 +237,6 @@ export class BidiBrowserContext extends BrowserContext {
await Promise.all(promises);
}
private async _installUtilityScript() {
await this._browser._browserSession.send('script.addPreloadScript', {
functionDeclaration: `() => { return${kUtilityInitScript.source} }`,
userContexts: [this._userContextId()],
});
}
override possiblyUninitializedPages(): Page[] {
return this._bidiPages().map(bidiPage => bidiPage._page);
}

View File

@ -21,7 +21,6 @@ import { BrowserContext, verifyGeolocation } from '../browserContext';
import { TargetClosedError } from '../errors';
import { kPlaywrightBinding } from '../javascript';
import * as network from '../network';
import { kUtilityInitScript } from '../page';
import { ConnectionEvents, FFConnection } from './ffConnection';
import { FFPage } from './ffPage';
@ -383,7 +382,7 @@ export class FFBrowserContext extends BrowserContext {
if (this.bindingsInitScript)
bindingScripts.unshift(this.bindingsInitScript.source);
const initScripts = this.initScripts.map(script => script.source);
await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: [kUtilityInitScript.source, ...bindingScripts, ...initScripts].map(script => ({ script })) });
await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: [...bindingScripts, ...initScripts].map(script => ({ script })) });
}
async doUpdateRequestInterception(): Promise<void> {

View File

@ -131,7 +131,7 @@ export class ExecutionContext extends SdkObject {
(() => {
const module = {};
${kUtilityScriptSource}
return (module.exports.ensureUtilityScript())();
return new (module.exports.UtilityScript())(globalThis);
})();`;
this._utilityScriptPromise = this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(this, source))
.then(handle => {

View File

@ -760,7 +760,7 @@ export class Page extends SdkObject {
const bindings = [...this.browserContext._pageBindings.values(), ...this._pageBindings.values()].map(binding => binding.initScript);
if (this.browserContext.bindingsInitScript)
bindings.unshift(this.browserContext.bindingsInitScript);
return [kUtilityInitScript, ...bindings, ...this.browserContext.initScripts, ...this.initScripts];
return [...bindings, ...this.browserContext.initScripts, ...this.initScripts];
}
getBinding(name: string) {
@ -902,14 +902,6 @@ export class InitScript {
}
}
export const kUtilityInitScript = new InitScript(`
(() => {
const module = {};
${js.kUtilityScriptSource}
(module.exports.ensureUtilityScript())();
})();
`, true /* internal */);
class FrameThrottler {
private _acks: (() => void)[] = [];
private _defaultInterval: number;

View File

@ -14,6 +14,12 @@
* limitations under the License.
*/
// Hopefully, this file is never used in injected sources,
// because it does not use `builtins.performance`,
// and can break when clock emulation is engaged.
/* eslint-disable no-restricted-globals */
let _timeOrigin = performance.timeOrigin;
let _timeShift = 0;

View File

@ -14,6 +14,12 @@
* limitations under the License.
*/
// Hopefully, this file is never used in injected sources,
// because it does not use `builtins.setTimeout` and similar,
// and can break when clock emulation is engaged.
/* eslint-disable no-restricted-globals */
import { monotonicTime } from './time';
export async function raceAgainstDeadline<T>(cb: () => Promise<T>, deadline: number): Promise<{ result: T, timedOut: false } | { timedOut: true }> {

View File

@ -45,8 +45,10 @@ function isRegExp(obj: any): obj is RegExp {
}
}
// eslint-disable-next-line no-restricted-globals
function isDate(obj: any): obj is Date {
try {
// eslint-disable-next-line no-restricted-globals
return obj instanceof Date || Object.prototype.toString.call(obj) === '[object Date]';
} catch (error) {
return false;
@ -132,8 +134,10 @@ export function parseEvaluationResultValue(value: SerializedValue, handles: any[
return -0;
return undefined;
}
if ('d' in value)
if ('d' in value) {
// eslint-disable-next-line no-restricted-globals
return new Date(value.d);
}
if ('u' in value)
return new URL(value.u);
if ('bi' in value)

View File

@ -16,21 +16,22 @@
import { test, expect } from '@playwright/test';
import { createClock as rawCreateClock, install as rawInstall } from '../../../packages/injected/src/clock';
import type { InstallConfig, ClockController, ClockMethods } from '../../../packages/injected/src/clock';
import type { InstallConfig, ClockController } from '../../../packages/injected/src/clock';
import type { Builtins } from '../../../packages/injected/src/utilityScript';
const createClock = (now?: number): ClockController & ClockMethods => {
const createClock = (now?: number): ClockController & Builtins => {
const { clock, api } = rawCreateClock(globalThis);
clock.setSystemTime(now || 0);
for (const key of Object.keys(api))
clock[key] = api[key];
return clock as ClockController & ClockMethods;
return clock as ClockController & Builtins;
};
type ClockFixtures = {
clock: ClockController & ClockMethods;
clock: ClockController & Builtins;
now: number | undefined;
install: (now?: number) => ClockController & ClockMethods;
installEx: (config?: InstallConfig) => { clock: ClockController, api: ClockMethods, originals: ClockMethods };
install: (now?: number) => ClockController & Builtins;
installEx: (config?: InstallConfig) => { clock: ClockController, api: Builtins, originals: Builtins };
};
const it = test.extend<ClockFixtures>({
@ -42,14 +43,14 @@ const it = test.extend<ClockFixtures>({
now: undefined,
install: async ({}, use) => {
let clockObject: ClockController & ClockMethods;
let clockObject: ClockController & Builtins;
const install = (now?: number) => {
const { clock, api } = rawInstall(globalThis);
if (now)
clock.setSystemTime(now);
for (const key of Object.keys(api))
clock[key] = api[key];
clockObject = clock as ClockController & ClockMethods;
clockObject = clock as ClockController & Builtins;
return clockObject;
};
await use(install);

View File

@ -83,15 +83,3 @@ it('should work with bogus Array.from', async ({ page, server }) => {
const divsCount = await page.$$eval('css=div', divs => divs.length);
expect(divsCount).toBe(3);
});
it('should work with broken Map', async ({ page, server }) => {
await page.setContent(`
<script>
window.Map = () => {};
</script>
<button>Click me</button>
<button>And me</button>
`);
const count = await page.$$eval('role=button', els => els.length);
expect(count).toBe(2);
});

View File

@ -109,3 +109,18 @@ it('init script should run only once in popup', async ({ page, browserName }) =>
]);
expect(await popup.evaluate('callCount')).toEqual(1);
});
it('init script should not observe playwright internals', async ({ server, page }) => {
it.skip(!!process.env.PW_CLOCK, 'clock installs globalThis.__pwClock');
await page.addInitScript(() => {
window['check'] = () => {
const keys = Reflect.ownKeys(globalThis).map(k => k.toString());
return keys.find(name => name.includes('playwright') || name.includes('_pw')) || 'none';
};
window['found'] = window['check']();
});
await page.goto(server.EMPTY_PAGE);
expect(await page.evaluate(() => window['found'])).toBe('none');
expect(await page.evaluate(() => window['check']())).toBe('none');
});

View File

@ -850,38 +850,6 @@ it('should work with Array.from/map', async ({ page }) => {
})).toBe('([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})');
});
it('should work with overridden eval', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34628' },
}, async ({ page, server }) => {
server.setRoute('/page', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`
<script>
window.eval = () => 42;
</script>
`);
});
await page.goto(server.PREFIX + '/page');
expect(await page.evaluate(x => ({ value: 2 * x }), 17)).toEqual({ value: 34 });
});
it('should work with deleted Map', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34443' },
}, async ({ page, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34443' });
server.setRoute('/page', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`
<script>
delete window.Map;
</script>
`);
});
await page.goto(server.PREFIX + '/page');
expect(await page.evaluate(x => ({ value: 2 * x }), 17)).toEqual({ value: 34 });
});
it('should ignore dangerous object keys', async ({ page }) => {
const input = {
__proto__: { polluted: true },

View File

@ -309,41 +309,3 @@ it('should fail with busted Array.prototype.toJSON', async ({ page }) => {
expect.soft(await page.evaluate(() => ([] as any).toJSON())).toBe('"[]"');
});
it('should work with overridden eval', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34628' },
}, async ({ page, server }) => {
await page.exposeFunction('add', (a, b) => a + b);
server.setRoute('/page', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`
<script>
window.eval = () => 42;
</script>
`);
});
await page.goto(server.PREFIX + '/page');
expect(await page.evaluate(async () => {
return { value: await (window as any)['add'](5, 6) };
})).toEqual({ value: 11 });
});
it('should work with deleted Map', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34443' },
}, async ({ page, server }) => {
await page.exposeFunction('add', (a, b) => a + b);
server.setRoute('/page', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`
<script>
delete window.Map;
</script>
`);
});
await page.goto(server.PREFIX + '/page');
expect(await page.evaluate(async () => {
return { value: await (window as any)['add'](5, 6) };
})).toEqual({ value: 11 });
});

View File

@ -136,7 +136,6 @@ 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);

View File

@ -1,48 +0,0 @@
/**
* 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',
};