From 6af41232b8825180f8716cf8fe99d26906570a1b Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 21 May 2025 13:45:50 +0000 Subject: [PATCH] chore: evaluate `UtilityScript` lazily (#36019) --- eslint.config.mjs | 12 ++++ packages/injected/src/clock.ts | 52 ++++++-------- packages/injected/src/highlight.ts | 6 +- packages/injected/src/injectedScript.ts | 13 ++-- packages/injected/src/recorder/recorder.ts | 4 +- packages/injected/src/utilityScript.ts | 71 ++++++------------- .../src/server/bidi/bidiBrowser.ts | 9 --- .../src/server/firefox/ffBrowser.ts | 3 +- .../playwright-core/src/server/javascript.ts | 2 +- packages/playwright-core/src/server/page.ts | 10 +-- .../src/utils/isomorphic/time.ts | 6 ++ .../src/utils/isomorphic/timeoutRunner.ts | 6 ++ .../isomorphic/utilityScriptSerializers.ts | 6 +- tests/library/unit/clock.spec.ts | 17 ++--- tests/page/eval-on-selector-all.spec.ts | 12 ---- tests/page/page-add-init-script.spec.ts | 15 ++++ tests/page/page-evaluate.spec.ts | 32 --------- tests/page/page-expose-function.spec.ts | 38 ---------- utils/generate_injected.js | 1 - utils/generate_injected_builtins.js | 48 ------------- 20 files changed, 110 insertions(+), 253 deletions(-) delete mode 100644 utils/generate_injected_builtins.js diff --git a/eslint.config.mjs b/eslint.config.mjs index 7a8933e530..26e7070a1f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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" }]; diff --git a/packages/injected/src/clock.ts b/packages/injected/src/clock.ts index 57519742f4..673a2da6fa 100644 --- a/packages/injected/src/clock.ts +++ b/packages/injected/src/clock.ts @@ -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, }; } diff --git a/packages/injected/src/highlight.ts b/packages/injected/src/highlight.ts index b3ebb98ae3..1bc011f357 100644 --- a/packages/injected/src/highlight.ts +++ b/packages/injected/src/highlight.ts @@ -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(); } diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index b60b442b18..2093d20ab9 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -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; } diff --git a/packages/injected/src/recorder/recorder.ts b/packages/injected/src/recorder/recorder.ts index a7b5507f38..68ba74fdb1 100644 --- a/packages/injected/src/recorder/recorder.ts +++ b/packages/injected/src/recorder/recorder.ts @@ -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); } diff --git a/packages/injected/src/utilityScript.ts b/packages/injected/src/utilityScript.ts index f44db607fe..c5751688a0 100644 --- a/packages/injected/src/utilityScript.ts +++ b/packages/injected/src/utilityScript.ts @@ -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; -} diff --git a/packages/playwright-core/src/server/bidi/bidiBrowser.ts b/packages/playwright-core/src/server/bidi/bidiBrowser.ts index 302b1e199f..1b0dd061f3 100644 --- a/packages/playwright-core/src/server/bidi/bidiBrowser.ts +++ b/packages/playwright-core/src/server/bidi/bidiBrowser.ts @@ -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[] = [ 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); } diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index df54bc82f3..3641bfea01 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -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 { diff --git a/packages/playwright-core/src/server/javascript.ts b/packages/playwright-core/src/server/javascript.ts index b9c108cd25..a680943637 100644 --- a/packages/playwright-core/src/server/javascript.ts +++ b/packages/playwright-core/src/server/javascript.ts @@ -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 => { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index ab2f432d30..f8e59b96d9 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -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; diff --git a/packages/playwright-core/src/utils/isomorphic/time.ts b/packages/playwright-core/src/utils/isomorphic/time.ts index 5db2c621f5..b5b68a59d5 100644 --- a/packages/playwright-core/src/utils/isomorphic/time.ts +++ b/packages/playwright-core/src/utils/isomorphic/time.ts @@ -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; diff --git a/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts b/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts index e8016ddb49..9a01d0f079 100644 --- a/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts +++ b/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts @@ -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(cb: () => Promise, deadline: number): Promise<{ result: T, timedOut: false } | { timedOut: true }> { diff --git a/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts b/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts index 0a65978032..36e2410489 100644 --- a/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts +++ b/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts @@ -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) diff --git a/tests/library/unit/clock.spec.ts b/tests/library/unit/clock.spec.ts index e31e0693ea..a346fe3d52 100644 --- a/tests/library/unit/clock.spec.ts +++ b/tests/library/unit/clock.spec.ts @@ -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({ @@ -42,14 +43,14 @@ const it = test.extend({ 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); diff --git a/tests/page/eval-on-selector-all.spec.ts b/tests/page/eval-on-selector-all.spec.ts index 5253eeda14..92fae9501b 100644 --- a/tests/page/eval-on-selector-all.spec.ts +++ b/tests/page/eval-on-selector-all.spec.ts @@ -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(` - - - - `); - const count = await page.$$eval('role=button', els => els.length); - expect(count).toBe(2); -}); diff --git a/tests/page/page-add-init-script.spec.ts b/tests/page/page-add-init-script.spec.ts index b2b7782eba..82d7230d86 100644 --- a/tests/page/page-add-init-script.spec.ts +++ b/tests/page/page-add-init-script.spec.ts @@ -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'); +}); diff --git a/tests/page/page-evaluate.spec.ts b/tests/page/page-evaluate.spec.ts index 2b82684fc0..c5d70f05cf 100644 --- a/tests/page/page-evaluate.spec.ts +++ b/tests/page/page-evaluate.spec.ts @@ -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(` - - `); - }); - 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(` - - `); - }); - 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 }, diff --git a/tests/page/page-expose-function.spec.ts b/tests/page/page-expose-function.spec.ts index 7af31a5785..b16546ef61 100644 --- a/tests/page/page-expose-function.spec.ts +++ b/tests/page/page-expose-function.spec.ts @@ -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(` - - `); - }); - 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(` - - `); - }); - await page.goto(server.PREFIX + '/page'); - expect(await page.evaluate(async () => { - return { value: await (window as any)['add'](5, 6) }; - })).toEqual({ value: 11 }); -}); diff --git a/utils/generate_injected.js b/utils/generate_injected.js index d872218525..73d52aaeb6 100644 --- a/utils/generate_injected.js +++ b/utils/generate_injected.js @@ -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); diff --git a/utils/generate_injected_builtins.js b/utils/generate_injected_builtins.js deleted file mode 100644 index 76837b1fc1..0000000000 --- a/utils/generate_injected_builtins.js +++ /dev/null @@ -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', -};