Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: builtins #35197

Merged
merged 2 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,25 @@ const noBooleanCompareRules = {
};

const noWebGlobalsRules = {
// This should contain every bulitin from builtins.ts.
"no-restricted-globals": [
"error",
{ name: "window" },
{ name: "document" },
{ name: "globalThis" },
{ "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.builtins.setTimeout instead" },
{ "name": "clearTimeout", "message": "Use InjectedScript.builtins.clearTimeout instead" },
{ "name": "setInterval", "message": "Use InjectedScript.builtins.setInterval instead" },
{ "name": "clearInterval", "message": "Use InjectedScript.builtins.clearInterval instead" },
{ "name": "requestAnimationFrame", "message": "Use InjectedScript.builtins.requestAnimationFrame instead" },
{ "name": "cancelAnimationFrame", "message": "Use InjectedScript.builtins.cancelAnimationFrame instead" },
{ "name": "requestIdleCallback", "message": "Use InjectedScript.builtins.requestIdleCallback instead" },
{ "name": "cancelIdleCallback", "message": "Use InjectedScript.builtins.cancelIdleCallback instead" },
{ "name": "performance", "message": "Use InjectedScript.builtins.performance instead" },
{ "name": "eval", "message": "Use InjectedScript.builtins.eval instead" },
{ "name": "Date", "message": "Use InjectedScript.builtins.Date instead" },
{ "name": "Map", "message": "Use InjectedScript.builtins.Map instead" },
{ "name": "Set", "message": "Use InjectedScript.builtins.Set instead" },
],
};

Expand Down Expand Up @@ -358,6 +372,8 @@ export default [
"packages/playwright-core/src/server/injected/**/*.ts",
"packages/playwright-core/src/server/isomorphic/**/*.ts",
"packages/playwright-core/src/utils/isomorphic/**/*.ts",
"packages/playwright-core/src/server/pageBinding.ts",
"packages/playwright-core/src/server/storageScript.ts",
],
languageOptions: languageOptionsWithTsConfig,
rules: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/

import { assert } from '../../utils';
import { parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers';
import * as js from '../javascript';
import * as dom from '../dom';
import { BidiDeserializer } from './third_party/bidiDeserializer';
Expand Down Expand Up @@ -98,7 +97,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 parseEvaluationResultValue(BidiDeserializer.deserialize(response.result));
return js.parseEvaluationResultValue(BidiDeserializer.deserialize(response.result));
return createHandle(utilityScript._context, response.result);
}
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
Expand Down
5 changes: 3 additions & 2 deletions packages/playwright-core/src/server/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { mkdirIfNeeded } from './utils/fileUtils';
import { HarRecorder } from './har/harRecorder';
import { helper } from './helper';
import { SdkObject, serverSideCallMetadata } from './instrumentation';
import { ensureBuiltins } from './isomorphic/builtins';
import * as utilityScriptSerializers from './isomorphic/utilityScriptSerializers';
import * as network from './network';
import { InitScript } from './page';
Expand Down Expand Up @@ -518,7 +519,7 @@ export abstract class BrowserContext extends SdkObject {
};
const originsToSave = new Set(this._origins);

const collectScript = `(${storageScript.collect})((${utilityScriptSerializers.source})(), ${this._browser.options.name === 'firefox'}, ${indexedDB})`;
const collectScript = `(${storageScript.collect})(${utilityScriptSerializers.source}, (${ensureBuiltins})(globalThis), ${this._browser.options.name === 'firefox'}, ${indexedDB})`;

// First try collecting storage stage from existing pages.
for (const page of this.pages()) {
Expand Down Expand Up @@ -611,7 +612,7 @@ export abstract class BrowserContext extends SdkObject {
for (const originState of state.origins) {
const frame = page.mainFrame();
await frame.goto(metadata, originState.origin);
await frame.evaluateExpression(`(${storageScript.restore})(${JSON.stringify(originState)}, (${utilityScriptSerializers.source})())`, { world: 'utility' });
await frame.evaluateExpression(`(${storageScript.restore})(${utilityScriptSerializers.source}, (${ensureBuiltins})(globalThis), ${JSON.stringify(originState)})`, { world: 'utility' });
}
await page.close(internalMetadata);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import { assert } from '../../utils/isomorphic/assert';
import { getExceptionMessage, releaseObject } from './crProtocolHelper';
import { rewriteErrorMessage } from '../../utils/isomorphic/stackTrace';
import { parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers';
import * as js from '../javascript';
import * as dom from '../dom';
import { isSessionClosedError } from '../protocolError';
Expand Down Expand Up @@ -71,7 +70,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
}).catch(rewriteError);
if (exceptionDetails)
throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails));
return returnByValue ? parseEvaluationResultValue(remoteObject.value) : createHandle(utilityScript._context, remoteObject);
return returnByValue ? js.parseEvaluationResultValue(remoteObject.value) : createHandle(utilityScript._context, remoteObject);
}

async getProperties(object: js.JSHandle): Promise<Map<string, js.JSHandle>> {
Expand Down
5 changes: 3 additions & 2 deletions packages/playwright-core/src/server/firefox/ffBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { Browser } from '../browser';
import { BrowserContext, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext';
import { TargetClosedError } from '../errors';
import * as network from '../network';
import { PageBinding } from '../page';
import { kBuiltinsScript, PageBinding } from '../page';
import { ConnectionEvents, FFConnection } from './ffConnection';
import { FFPage } from './ffPage';

Expand Down Expand Up @@ -185,6 +185,7 @@ export class FFBrowserContext extends BrowserContext {
const promises: Promise<any>[] = [
super._initialize(),
this._browser.session.send('Browser.addBinding', { browserContextId: this._browserContextId, name: PageBinding.kPlaywrightBinding, script: '' }),
this._updateInitScripts(),
];
if (this._options.acceptDownloads !== 'internal-browser-default') {
promises.push(this._browser.session.send('Browser.setDownloadOptions', {
Expand Down Expand Up @@ -377,7 +378,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: [...bindingScripts, ...initScripts].map(script => ({ script })) });
await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: [kBuiltinsScript.source, ...bindingScripts, ...initScripts].map(script => ({ script })) });
}

async doUpdateRequestInterception(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

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

Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1567,9 +1567,9 @@ export class Frame extends SdkObject {
return;
}
if (typeof polling !== 'number')
injected.builtinRequestAnimationFrame(next);
injected.builtins.requestAnimationFrame(next);
else
injected.builtinSetTimeout(next, polling);
injected.builtins.setTimeout(next, polling);
} catch (e) {
reject(e);
}
Expand Down
29 changes: 15 additions & 14 deletions packages/playwright-core/src/server/injected/ariaSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import * as roleUtils from './roleUtils';
import { yamlEscapeKeyIfNeeded, yamlEscapeValueIfNeeded } from './yaml';

import type { AriaProps, AriaRegex, AriaRole, AriaTemplateNode, AriaTemplateRoleNode, AriaTemplateTextNode } from '@isomorphic/ariaSnapshot';
import type { Builtins } from '../isomorphic/builtins';

export type AriaNode = AriaProps & {
role: AriaRole | 'fragment';
Expand All @@ -32,19 +33,19 @@ export type AriaNode = AriaProps & {

export type AriaSnapshot = {
root: AriaNode;
elements: Map<number, Element>;
elements: Builtins.Map<number, Element>;
generation: number;
ids: Map<Element, number>;
ids: Builtins.Map<Element, number>;
};

export function generateAriaTree(rootElement: Element, generation: number): AriaSnapshot {
const visited = new Set<Node>();
export function generateAriaTree(builtins: Builtins, rootElement: Element, generation: number): AriaSnapshot {
const visited = new builtins.Set<Node>();

const snapshot: AriaSnapshot = {
root: { role: 'fragment', name: '', children: [], element: rootElement, props: {} },
elements: new Map<number, Element>(),
elements: new builtins.Map<number, Element>(),
generation,
ids: new Map<Element, number>(),
ids: new builtins.Map<Element, number>(),
};

const addElement = (element: Element) => {
Expand Down Expand Up @@ -86,7 +87,7 @@ export function generateAriaTree(rootElement: Element, generation: number): Aria
}

addElement(element);
const childAriaNode = toAriaNode(element);
const childAriaNode = toAriaNode(builtins, element);
if (childAriaNode)
ariaNode.children.push(childAriaNode);
processElement(childAriaNode || ariaNode, element, ariaChildren);
Expand Down Expand Up @@ -132,7 +133,7 @@ export function generateAriaTree(rootElement: Element, generation: number): Aria
}
}

roleUtils.beginAriaCaches();
roleUtils.beginAriaCaches(builtins);
try {
visit(snapshot.root, rootElement);
} finally {
Expand All @@ -143,12 +144,12 @@ export function generateAriaTree(rootElement: Element, generation: number): Aria
return snapshot;
}

function toAriaNode(element: Element): AriaNode | null {
function toAriaNode(builtins: Builtins, element: Element): AriaNode | null {
const role = roleUtils.getAriaRole(element);
if (!role || role === 'presentation' || role === 'none')
return null;

const name = normalizeWhiteSpace(roleUtils.getElementAccessibleName(element, false) || '');
const name = normalizeWhiteSpace(roleUtils.getElementAccessibleName(builtins, element, false) || '');
const result: AriaNode = { role, name, children: [], props: {}, element };

if (roleUtils.kAriaCheckedRoles.includes(role))
Expand Down Expand Up @@ -230,8 +231,8 @@ export type MatcherReceived = {
regex: string;
};

export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: AriaNode[], received: MatcherReceived } {
const snapshot = generateAriaTree(rootElement, 0);
export function matchesAriaTree(builtins: Builtins, rootElement: Element, template: AriaTemplateNode): { matches: AriaNode[], received: MatcherReceived } {
const snapshot = generateAriaTree(builtins, rootElement, 0);
const matches = matchesNodeDeep(snapshot.root, template, false);
return {
matches,
Expand All @@ -242,8 +243,8 @@ export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode
};
}

export function getAllByAria(rootElement: Element, template: AriaTemplateNode): Element[] {
const root = generateAriaTree(rootElement, 0).root;
export function getAllByAria(builtins: Builtins, rootElement: Element, template: AriaTemplateNode): Element[] {
const root = generateAriaTree(builtins, rootElement, 0).root;
const matches = matchesNodeDeep(root, template, true);
return matches.map(n => n.element);
}
Expand Down
20 changes: 13 additions & 7 deletions packages/playwright-core/src/server/injected/clock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
* 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 { ensureBuiltins } from '../isomorphic/builtins';

import type { Builtins } from '../isomorphic/builtins';

export type ClockMethods = {
Date: DateConstructor;
setTimeout: Window['setTimeout'];
Expand All @@ -25,7 +29,7 @@ export type ClockMethods = {
};

export type ClockConfig = {
now?: number | Date;
now?: number | Builtins.Date;
};

export type InstallConfig = ClockConfig & {
Expand Down Expand Up @@ -74,15 +78,16 @@ type LogEntryType = 'fastForward' |'install' | 'pauseAt' | 'resume' | 'runFor' |
export class ClockController {
readonly _now: Time;
private _duringTick = false;
private _timers = new Map<number, Timer>();
private _timers: Builtins.Map<number, Timer>;
private _uniqueTimerId = idCounterStart;
private _embedder: Embedder;
readonly disposables: (() => void)[] = [];
private _log: { type: LogEntryType, time: number, param?: number }[] = [];
private _realTime: { startTicks: EmbedderTicks, lastSyncTicks: EmbedderTicks } | undefined;
private _currentRealTimeTimer: { callAt: Ticks, dispose: () => void } | undefined;

constructor(embedder: Embedder) {
constructor(builtins: Builtins, embedder: Embedder) {
this._timers = new builtins.Map();
this._now = { time: asWallTime(0), isFixedTime: false, ticks: 0 as Ticks, origin: asWallTime(-1) };
this._embedder = embedder;
}
Expand Down Expand Up @@ -416,7 +421,7 @@ export class ClockController {
}
}

function mirrorDateProperties(target: any, source: typeof Date): DateConstructor & Date {
function mirrorDateProperties(target: any, source: DateConstructor): DateConstructor & Builtins.Date {
for (const prop in source) {
if (source.hasOwnProperty(prop))
target[prop] = (source as any)[prop];
Expand All @@ -430,8 +435,8 @@ function mirrorDateProperties(target: any, source: typeof Date): DateConstructor
return target;
}

function createDate(clock: ClockController, NativeDate: typeof Date): DateConstructor & Date {
function ClockDate(this: typeof ClockDate, year: number, month: number, date: number, hour: number, minute: number, second: number, ms: number): Date | string {
function createDate(clock: ClockController, NativeDate: DateConstructor): DateConstructor & Builtins.Date {
function ClockDate(this: typeof ClockDate, year: number, month: number, date: number, hour: number, minute: number, second: number, ms: number): Builtins.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.
if (!(this instanceof ClockDate))
Expand Down Expand Up @@ -680,7 +685,8 @@ export function createClock(globalObject: WindowOrWorkerGlobalScope): { clock: C
},
};

const clock = new ClockController(embedder);
// TODO: unify ensureBuiltins and platformOriginals
const clock = new ClockController(ensureBuiltins(globalObject as any), embedder);
const api = createApi(clock, originals.bound);
return { clock, api, originals: originals.raw };
}
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-core/src/server/injected/highlight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,20 @@ export class Highlight {

runHighlightOnRaf(selector: ParsedSelector) {
if (this._rafRequest)
cancelAnimationFrame(this._rafRequest);
this._injectedScript.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';
this.updateHighlight(elements.map((element, index) => {
const suffix = elements.length > 1 ? ` [${index + 1} of ${elements.length}]` : '';
return { element, color, tooltipText: locator + suffix };
}));
this._rafRequest = this._injectedScript.builtinRequestAnimationFrame(() => this.runHighlightOnRaf(selector));
this._rafRequest = this._injectedScript.builtins.requestAnimationFrame(() => this.runHighlightOnRaf(selector));
}

uninstall() {
if (this._rafRequest)
cancelAnimationFrame(this._rafRequest);
this._injectedScript.builtins.cancelAnimationFrame(this._rafRequest);
this._glassPaneElement.remove();
}

Expand Down
Loading
Loading