From 2faf9ffdb0437661136696417c22cb94fec73000 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 4 Apr 2025 13:56:20 +0100 Subject: [PATCH 1/3] chore: use a singleton "builtins" instances instead of passing it around --- packages/injected/src/ariaSnapshot.ts | 25 +++++------ packages/injected/src/clock.ts | 8 ++-- packages/injected/src/highlight.ts | 7 ++-- packages/injected/src/injectedScript.ts | 42 ++++++++++--------- packages/injected/src/reactSelectorEngine.ts | 5 ++- packages/injected/src/roleSelectorEngine.ts | 11 +++-- packages/injected/src/roleUtils.ts | 40 +++++++++--------- packages/injected/src/selectorEvaluator.ts | 38 ++++++++--------- packages/injected/src/selectorGenerator.ts | 9 ++-- packages/injected/src/utilityScript.ts | 13 ++---- packages/injected/src/vueSelectorEngine.ts | 14 +++---- packages/injected/src/webSocketMock.ts | 6 +-- .../src/server/browserContext.ts | 6 +-- .../playwright-core/src/server/javascript.ts | 4 +- packages/playwright-core/src/server/page.ts | 4 +- .../playwright-core/src/server/pageBinding.ts | 4 +- .../src/utils/isomorphic/builtins.ts | 3 +- tests/library/role-utils.spec.ts | 8 ++-- 18 files changed, 121 insertions(+), 126 deletions(-) diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index 0a1bb08fef053..021b3c86eb80d 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { builtins } from '@isomorphic/builtins'; import { escapeRegExp, longestCommonSubstring, normalizeWhiteSpace } from '@isomorphic/stringUtils'; import { getElementComputedStyle, getGlobalOptions } from './domUtils'; @@ -38,14 +39,14 @@ export type AriaSnapshot = { ids: Builtins.Map; }; -export function generateAriaTree(builtins: Builtins, rootElement: Element, generation: number): AriaSnapshot { - const visited = new builtins.Set(); +export function generateAriaTree(rootElement: Element, generation: number): AriaSnapshot { + const visited = new (builtins().Set)(); const snapshot: AriaSnapshot = { root: { role: 'fragment', name: '', children: [], element: rootElement, props: {} }, - elements: new builtins.Map(), + elements: new (builtins().Map)(), generation, - ids: new builtins.Map(), + ids: new (builtins().Map)(), }; const addElement = (element: Element) => { @@ -87,7 +88,7 @@ export function generateAriaTree(builtins: Builtins, rootElement: Element, gener } addElement(element); - const childAriaNode = toAriaNode(builtins, element); + const childAriaNode = toAriaNode(element); if (childAriaNode) ariaNode.children.push(childAriaNode); processElement(childAriaNode || ariaNode, element, ariaChildren); @@ -133,7 +134,7 @@ export function generateAriaTree(builtins: Builtins, rootElement: Element, gener } } - roleUtils.beginAriaCaches(builtins); + roleUtils.beginAriaCaches(); try { visit(snapshot.root, rootElement); } finally { @@ -144,7 +145,7 @@ export function generateAriaTree(builtins: Builtins, rootElement: Element, gener return snapshot; } -function toAriaNode(builtins: Builtins, element: Element): AriaNode | null { +function toAriaNode(element: Element): AriaNode | null { if (element.nodeName === 'IFRAME') return { role: 'iframe', name: '', children: [], props: {}, element }; @@ -152,7 +153,7 @@ function toAriaNode(builtins: Builtins, element: Element): AriaNode | null { if (!role || role === 'presentation' || role === 'none') return null; - const name = normalizeWhiteSpace(roleUtils.getElementAccessibleName(builtins, element, false) || ''); + const name = normalizeWhiteSpace(roleUtils.getElementAccessibleName(element, false) || ''); const result: AriaNode = { role, name, children: [], props: {}, element }; if (roleUtils.kAriaCheckedRoles.includes(role)) @@ -234,8 +235,8 @@ export type MatcherReceived = { regex: string; }; -export function matchesAriaTree(builtins: Builtins, rootElement: Element, template: AriaTemplateNode): { matches: AriaNode[], received: MatcherReceived } { - const snapshot = generateAriaTree(builtins, rootElement, 0); +export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: AriaNode[], received: MatcherReceived } { + const snapshot = generateAriaTree(rootElement, 0); const matches = matchesNodeDeep(snapshot.root, template, false, false); return { matches, @@ -246,8 +247,8 @@ export function matchesAriaTree(builtins: Builtins, rootElement: Element, templa }; } -export function getAllByAria(builtins: Builtins, rootElement: Element, template: AriaTemplateNode): Element[] { - const root = generateAriaTree(builtins, rootElement, 0).root; +export function getAllByAria(rootElement: Element, template: AriaTemplateNode): Element[] { + const root = generateAriaTree(rootElement, 0).root; const matches = matchesNodeDeep(root, template, true, false); return matches.map(n => n.element); } diff --git a/packages/injected/src/clock.ts b/packages/injected/src/clock.ts index cafdc9583eb39..f198023302aa6 100644 --- a/packages/injected/src/clock.ts +++ b/packages/injected/src/clock.ts @@ -10,7 +10,7 @@ * 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 { builtins } from '@isomorphic/builtins'; import type { Builtins } from '@isomorphic/builtins'; @@ -86,8 +86,8 @@ export class ClockController { private _realTime: { startTicks: EmbedderTicks, lastSyncTicks: EmbedderTicks } | undefined; private _currentRealTimeTimer: { callAt: Ticks, dispose: () => void } | undefined; - constructor(builtins: Builtins, embedder: Embedder) { - this._timers = new builtins.Map(); + constructor(embedder: Embedder) { + this._timers = new (builtins().Map)(); this._now = { time: asWallTime(0), isFixedTime: false, ticks: 0 as Ticks, origin: asWallTime(-1) }; this._embedder = embedder; } @@ -696,7 +696,7 @@ export function createClock(globalObject: WindowOrWorkerGlobalScope): { clock: C }; // TODO: unify ensureBuiltins and platformOriginals - const clock = new ClockController(ensureBuiltins(globalObject as any), embedder); + const clock = new ClockController(embedder); const api = createApi(clock, originals.bound); return { clock, api, originals: originals.raw }; } diff --git a/packages/injected/src/highlight.ts b/packages/injected/src/highlight.ts index 400f52625d8cd..20e971b6d2667 100644 --- a/packages/injected/src/highlight.ts +++ b/packages/injected/src/highlight.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { builtins } from '@isomorphic/builtins'; import { asLocator } from '@isomorphic/locatorGenerators'; import { stringifySelector } from '@isomorphic/selectorParser'; @@ -100,7 +101,7 @@ export class Highlight { runHighlightOnRaf(selector: ParsedSelector) { if (this._rafRequest) - this._injectedScript.builtins.cancelAnimationFrame(this._rafRequest); + 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 +109,12 @@ export class Highlight { const suffix = elements.length > 1 ? ` [${index + 1} of ${elements.length}]` : ''; return { element, color, tooltipText: locator + suffix }; })); - this._rafRequest = this._injectedScript.builtins.requestAnimationFrame(() => this.runHighlightOnRaf(selector)); + this._rafRequest = builtins().requestAnimationFrame(() => this.runHighlightOnRaf(selector)); } uninstall() { if (this._rafRequest) - this._injectedScript.builtins.cancelAnimationFrame(this._rafRequest); + builtins().cancelAnimationFrame(this._rafRequest); this._glassPaneElement.remove(); } diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index 0e5d07bcf2da8..ee428d10b07df 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -15,7 +15,7 @@ */ import { parseAriaSnapshot } from '@isomorphic/ariaSnapshot'; -import { ensureBuiltins } from '@isomorphic/builtins'; +import { builtins } from '@isomorphic/builtins'; import { asLocator } from '@isomorphic/locatorGenerators'; import { parseAttributeSelector, parseSelector, stringifySelector, visitAllSelectorParts } from '@isomorphic/selectorParser'; import { cacheNormalizedWhitespaces, normalizeWhiteSpace, trimStringWithEllipsis } from '@isomorphic/stringUtils'; @@ -111,10 +111,12 @@ export class InjectedScript { this.window = window; this.document = window.document; this.isUnderTest = isUnderTest; - this.builtins = ensureBuiltins(window); + // Make sure builtins are created from "window". This is important for InjectedScript instantiated + // inside a trace viewer snapshot, where "window" differs from "globalThis". + this.builtins = builtins(window); this._sdkLanguage = sdkLanguage; this._testIdAttributeNameForStrictErrorAndConsoleCodegen = testIdAttributeNameForStrictErrorAndConsoleCodegen; - this._evaluator = new SelectorEvaluatorImpl(this.builtins); + this._evaluator = new SelectorEvaluatorImpl(); this.onGlobalListenersRemoved = new this.builtins.Set(); this._autoClosingTags = new this.builtins.Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']); @@ -181,9 +183,9 @@ export class InjectedScript { this._engines = new this.builtins.Map(); this._engines.set('xpath', XPathEngine); this._engines.set('xpath:light', XPathEngine); - this._engines.set('_react', createReactEngine(this.builtins)); - this._engines.set('_vue', createVueEngine(this.builtins)); - this._engines.set('role', createRoleEngine(this.builtins, false)); + this._engines.set('_react', createReactEngine()); + this._engines.set('_vue', createVueEngine()); + this._engines.set('role', createRoleEngine(false)); this._engines.set('text', this._createTextEngine(true, false)); this._engines.set('text:light', this._createTextEngine(false, false)); this._engines.set('id', this._createAttributeEngine('id', true)); @@ -209,7 +211,7 @@ export class InjectedScript { this._engines.set('internal:has-not-text', this._createInternalHasNotTextEngine()); this._engines.set('internal:attr', this._createNamedAttributeEngine()); this._engines.set('internal:testid', this._createNamedAttributeEngine()); - this._engines.set('internal:role', createRoleEngine(this.builtins, true)); + this._engines.set('internal:role', createRoleEngine(true)); this._engines.set('aria-ref', this._createAriaIdEngine()); for (const { name, engine } of customEngines) @@ -284,7 +286,7 @@ export class InjectedScript { if (node.nodeType !== Node.ELEMENT_NODE) throw this.createStacklessError('Can only capture aria snapshot of Element nodes.'); const generation = (this._lastAriaSnapshot?.generation || 0) + 1; - this._lastAriaSnapshot = generateAriaTree(this.builtins, node as Element, generation); + this._lastAriaSnapshot = generateAriaTree(node as Element, generation); return renderAriaTree(this._lastAriaSnapshot, options); } @@ -293,7 +295,7 @@ export class InjectedScript { } getAllByAria(document: Document, template: AriaTemplateNode): Element[] { - return getAllByAria(this.builtins, document.documentElement, template); + return getAllByAria(document.documentElement, template); } querySelectorAll(selector: ParsedSelector, root: Node): Element[] { @@ -334,7 +336,7 @@ export class InjectedScript { roots = new this.builtins.Set(andElements.filter(e => roots.has(e))); } else if (part.name === 'internal:or') { const orElements = this.querySelectorAll((part.body as NestedSelectorBody).parsed, root); - roots = new this.builtins.Set(sortInDOMOrder(this.builtins, new this.builtins.Set([...roots, ...orElements]))); + roots = new this.builtins.Set(sortInDOMOrder(new this.builtins.Set([...roots, ...orElements]))); } else if (kLayoutSelectorNames.includes(part.name as LayoutSelectorName)) { roots = this._queryLayoutSelector(roots, part, root); } else { @@ -1430,13 +1432,13 @@ export class InjectedScript { const received = [...(element as HTMLSelectElement).selectedOptions].map(o => o.value); if (received.length !== options.expectedText!.length) return { received, matches: false }; - return { received, matches: received.map((r, i) => new ExpectedTextMatcher(this.builtins, options.expectedText![i]).matches(r)).every(Boolean) }; + return { received, matches: received.map((r, i) => new ExpectedTextMatcher(options.expectedText![i]).matches(r)).every(Boolean) }; } } { if (expression === 'to.match.aria') { - const result = matchesAriaTree(this.builtins, element, options.expectedValue); + const result = matchesAriaTree(element, options.expectedValue); return { received: result.received, matches: !!result.matches.length, @@ -1457,7 +1459,7 @@ export class InjectedScript { throw this.createStacklessError('Expected text is not provided for ' + expression); return { received: element.classList.toString(), - matches: new ExpectedTextMatcher(this.builtins, options.expectedText[0]).matchesClassList(this, element.classList, /* partial */ expression === 'to.contain.class'), + matches: new ExpectedTextMatcher(options.expectedText[0]).matchesClassList(this, element.classList, /* partial */ expression === 'to.contain.class'), }; } else if (expression === 'to.have.css') { received = this.window.getComputedStyle(element).getPropertyValue(options.expressionArg); @@ -1466,11 +1468,11 @@ export class InjectedScript { } else if (expression === 'to.have.text') { received = options.useInnerText ? (element as HTMLElement).innerText : elementText(new this.builtins.Map(), element).full; } else if (expression === 'to.have.accessible.name') { - received = getElementAccessibleName(this.builtins, element, false /* includeHidden */); + received = getElementAccessibleName(element, false /* includeHidden */); } else if (expression === 'to.have.accessible.description') { - received = getElementAccessibleDescription(this.builtins, element, false /* includeHidden */); + received = getElementAccessibleDescription(element, false /* includeHidden */); } else if (expression === 'to.have.accessible.error.message') { - received = getElementAccessibleErrorMessage(this.builtins, element); + received = getElementAccessibleErrorMessage(element); } else if (expression === 'to.have.role') { received = getAriaRole(element) || ''; } else if (expression === 'to.have.title') { @@ -1485,7 +1487,7 @@ export class InjectedScript { } if (received !== undefined && options.expectedText) { - const matcher = new ExpectedTextMatcher(this.builtins, options.expectedText[0]); + const matcher = new ExpectedTextMatcher(options.expectedText[0]); return { received, matches: matcher.matches(received) }; } } @@ -1539,7 +1541,7 @@ export class InjectedScript { received: T[], matchFn: (matcher: ExpectedTextMatcher, received: T) => boolean ): boolean { - const matchers = expectedText.map(e => new ExpectedTextMatcher(this.builtins, e)); + const matchers = expectedText.map(e => new ExpectedTextMatcher(e)); let mIndex = 0; let rIndex = 0; while (mIndex < matchers.length && rIndex < received.length) { @@ -1614,13 +1616,13 @@ class ExpectedTextMatcher { private _normalizeWhiteSpace: boolean | undefined; private _ignoreCase: boolean | undefined; - constructor(builtins: Builtins, expected: channels.ExpectedTextValue) { + constructor(expected: channels.ExpectedTextValue) { this._normalizeWhiteSpace = expected.normalizeWhiteSpace; this._ignoreCase = expected.ignoreCase; this._string = expected.matchSubstring ? undefined : this.normalize(expected.string); this._substring = expected.matchSubstring ? this.normalize(expected.string) : undefined; if (expected.regexSource) { - const flags = new builtins.Set((expected.regexFlags || '').split('')); + const flags = new (builtins().Set)((expected.regexFlags || '').split('')); if (expected.ignoreCase === false) flags.delete('i'); if (expected.ignoreCase === true) diff --git a/packages/injected/src/reactSelectorEngine.ts b/packages/injected/src/reactSelectorEngine.ts index 86896159aa029..7d2cc5b809cb1 100644 --- a/packages/injected/src/reactSelectorEngine.ts +++ b/packages/injected/src/reactSelectorEngine.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { builtins } from '@isomorphic/builtins'; import { parseAttributeSelector } from '@isomorphic/selectorParser'; import { isInsideScope } from './domUtils'; @@ -193,7 +194,7 @@ function findReactRoots(root: Document | ShadowRoot, roots: ReactVNode[] = []): return roots; } -export const createReactEngine = (builtins: Builtins): SelectorEngine => ({ +export const createReactEngine = (): SelectorEngine => ({ queryAll(scope: SelectorRoot, selector: string): Element[] { const { name, attributes } = parseAttributeSelector(selector, false); @@ -215,7 +216,7 @@ export const createReactEngine = (builtins: Builtins): SelectorEngine => ({ } return true; })).flat(); - const allRootElements: Builtins.Set = new builtins.Set(); + const allRootElements: Builtins.Set = new (builtins().Set)(); for (const treeNode of treeNodes) { for (const domNode of treeNode.rootElements) allRootElements.add(domNode); diff --git a/packages/injected/src/roleSelectorEngine.ts b/packages/injected/src/roleSelectorEngine.ts index 26d663c362e08..cbef4ed747774 100644 --- a/packages/injected/src/roleSelectorEngine.ts +++ b/packages/injected/src/roleSelectorEngine.ts @@ -20,7 +20,6 @@ import { normalizeWhiteSpace } from '@isomorphic/stringUtils'; import { beginAriaCaches, endAriaCaches, getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaRole, getAriaSelected, getElementAccessibleName, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils'; import { matchesAttributePart } from './selectorUtils'; -import type { Builtins } from '@isomorphic/builtins'; import type { AttributeSelectorOperator, AttributeSelectorPart } from '@isomorphic/selectorParser'; import type { SelectorEngine, SelectorRoot } from './selectorEngine'; @@ -128,7 +127,7 @@ function validateAttributes(attrs: AttributeSelectorPart[], role: string): RoleE return options; } -function queryRole(builtins: Builtins, scope: SelectorRoot, options: RoleEngineOptions, internal: boolean): Element[] { +function queryRole(scope: SelectorRoot, options: RoleEngineOptions, internal: boolean): Element[] { const result: Element[] = []; const match = (element: Element) => { if (getAriaRole(element) !== options.role) @@ -152,7 +151,7 @@ function queryRole(builtins: Builtins, scope: SelectorRoot, options: RoleEngineO } if (options.name !== undefined) { // Always normalize whitespace in the accessible name. - const accessibleName = normalizeWhiteSpace(getElementAccessibleName(builtins, element, !!options.includeHidden)); + const accessibleName = normalizeWhiteSpace(getElementAccessibleName(element, !!options.includeHidden)); if (typeof options.name === 'string') options.name = normalizeWhiteSpace(options.name); // internal:role assumes that [name="foo"i] also means substring. @@ -180,7 +179,7 @@ function queryRole(builtins: Builtins, scope: SelectorRoot, options: RoleEngineO return result; } -export function createRoleEngine(builtins: Builtins, internal: boolean): SelectorEngine { +export function createRoleEngine(internal: boolean): SelectorEngine { return { queryAll: (scope: SelectorRoot, selector: string): Element[] => { const parsed = parseAttributeSelector(selector, true); @@ -188,9 +187,9 @@ export function createRoleEngine(builtins: Builtins, internal: boolean): Selecto if (!role) throw new Error(`Role must not be empty`); const options = validateAttributes(parsed.attributes, role); - beginAriaCaches(builtins); + beginAriaCaches(); try { - return queryRole(builtins, scope, options, internal); + return queryRole(scope, options, internal); } finally { endAriaCaches(); } diff --git a/packages/injected/src/roleUtils.ts b/packages/injected/src/roleUtils.ts index 87a4791441674..23e7baf1a4b38 100644 --- a/packages/injected/src/roleUtils.ts +++ b/packages/injected/src/roleUtils.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { builtins } from '@isomorphic/builtins'; + import { getGlobalOptions, closestCrossShadow, elementSafeTagName, enclosingShadowRootOrDocument, getElementComputedStyle, isElementStyleVisibilityVisible, isVisibleTextNode, parentElementOrShadowHost } from './domUtils'; import type { AriaRole } from '@isomorphic/ariaSnapshot'; @@ -417,7 +419,7 @@ function allowsNameFromContent(role: string, targetDescendant: boolean) { return alwaysAllowsNameFromContent || descendantAllowsNameFromContent; } -export function getElementAccessibleName(builtins: Builtins, element: Element, includeHidden: boolean): string { +export function getElementAccessibleName(element: Element, includeHidden: boolean): string { const cache = (includeHidden ? cacheAccessibleNameHidden : cacheAccessibleName); let accessibleName = cache?.get(element); @@ -432,9 +434,8 @@ export function getElementAccessibleName(builtins: Builtins, element: Element, i if (!elementProhibitsNaming) { // step 2. accessibleName = asFlatString(getTextAlternativeInternal(element, { - builtins, includeHidden, - visitedElements: new builtins.Set(), + visitedElements: new (builtins().Set)(), embeddedInTargetElement: 'self', })); } @@ -444,7 +445,7 @@ export function getElementAccessibleName(builtins: Builtins, element: Element, i return accessibleName; } -export function getElementAccessibleDescription(builtins: Builtins, element: Element, includeHidden: boolean): string { +export function getElementAccessibleDescription(element: Element, includeHidden: boolean): string { const cache = (includeHidden ? cacheAccessibleDescriptionHidden : cacheAccessibleDescription); let accessibleDescription = cache?.get(element); @@ -457,9 +458,8 @@ export function getElementAccessibleDescription(builtins: Builtins, element: Ele // precedence 1 const describedBy = getIdRefs(element, element.getAttribute('aria-describedby')); accessibleDescription = asFlatString(describedBy.map(ref => getTextAlternativeInternal(ref, { - builtins, includeHidden, - visitedElements: new builtins.Set(), + visitedElements: new (builtins().Set)(), embeddedInDescribedBy: { element: ref, hidden: isElementHiddenForAria(ref) }, })).join(' ')); } else if (element.hasAttribute('aria-description')) { @@ -500,7 +500,7 @@ function getValidityInvalid(element: Element) { return false; } -export function getElementAccessibleErrorMessage(builtins: Builtins, element: Element): string { +export function getElementAccessibleErrorMessage(element: Element): string { // SPEC: https://w3c.github.io/aria/#aria-errormessage // // TODO: support https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/validationMessage @@ -519,8 +519,7 @@ export function getElementAccessibleErrorMessage(builtins: Builtins, element: El // Relevant vague spec: https://w3c.github.io/core-aam/#ariaErrorMessage. const parts = errorMessages.map(errorMessage => asFlatString( getTextAlternativeInternal(errorMessage, { - builtins, - visitedElements: new builtins.Set(), + visitedElements: new (builtins().Set)(), embeddedInDescribedBy: { element: errorMessage, hidden: isElementHiddenForAria(errorMessage) }, }) )); @@ -532,7 +531,6 @@ export function getElementAccessibleErrorMessage(builtins: Builtins, element: El } type AccessibleNameOptions = { - builtins: Builtins, visitedElements: Builtins.Set, includeHidden?: boolean, embeddedInDescribedBy?: { element: Element, hidden: boolean }, @@ -843,7 +841,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt !!options.embeddedInLabelledBy || !!options.embeddedInDescribedBy || !!options.embeddedInLabel || !!options.embeddedInNativeTextAlternative) { options.visitedElements.add(element); - const accessibleName = innerAccumulatedElementText(options.builtins, element, childOptions); + const accessibleName = innerAccumulatedElementText(element, childOptions); // Spec says "Return the accumulated text if it is not the empty string". However, that is not really // compatible with the real browser behavior and wpt tests, where an element with empty contents will fallback to the title. // So we follow the spec everywhere except for the target element itself. This can probably be improved. @@ -864,7 +862,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt return ''; } -function innerAccumulatedElementText(builtins: Builtins, element: Element, options: AccessibleNameOptions): string { +function innerAccumulatedElementText(element: Element, options: AccessibleNameOptions): string { const tokens: string[] = []; const visit = (node: Node, skipSlotted: boolean) => { if (skipSlotted && (node as Element | Text).assignedSlot) @@ -1066,16 +1064,16 @@ let cachePseudoContentBefore: Builtins.Map | undefined; let cachePseudoContentAfter: Builtins.Map | undefined; let cachesCounter = 0; -export function beginAriaCaches(builtins: Builtins) { +export function beginAriaCaches() { ++cachesCounter; - cacheAccessibleName ??= new builtins.Map(); - cacheAccessibleNameHidden ??= new builtins.Map(); - cacheAccessibleDescription ??= new builtins.Map(); - cacheAccessibleDescriptionHidden ??= new builtins.Map(); - cacheAccessibleErrorMessage ??= new builtins.Map(); - cacheIsHidden ??= new builtins.Map(); - cachePseudoContentBefore ??= new builtins.Map(); - cachePseudoContentAfter ??= new builtins.Map(); + cacheAccessibleName ??= new (builtins().Map)(); + cacheAccessibleNameHidden ??= new (builtins().Map)(); + cacheAccessibleDescription ??= new (builtins().Map)(); + cacheAccessibleDescriptionHidden ??= new (builtins().Map)(); + cacheAccessibleErrorMessage ??= new (builtins().Map)(); + cacheIsHidden ??= new (builtins().Map)(); + cachePseudoContentBefore ??= new (builtins().Map)(); + cachePseudoContentAfter ??= new (builtins().Map)(); } export function endAriaCaches() { diff --git a/packages/injected/src/selectorEvaluator.ts b/packages/injected/src/selectorEvaluator.ts index 2f78972fc51a1..2bb688700bcde 100644 --- a/packages/injected/src/selectorEvaluator.ts +++ b/packages/injected/src/selectorEvaluator.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { builtins } from '@isomorphic/builtins'; import { customCSSNames } from '@isomorphic/selectorParser'; import { normalizeWhiteSpace } from '@isomorphic/stringUtils'; @@ -35,7 +36,6 @@ type QueryContext = { }; export type Selector = any; // Opaque selector type. export interface SelectorEvaluator { - readonly builtins: Builtins; query(context: QueryContext, selector: Selector): Element[]; matches(element: Element, selector: Selector, context: QueryContext): boolean; } @@ -47,7 +47,6 @@ export interface SelectorEngine { type QueryCache = Builtins.Map; export class SelectorEvaluatorImpl implements SelectorEvaluator { - readonly builtins: Builtins; private _engines: Builtins.Map; private _cacheQueryCSS: QueryCache; private _cacheMatches: QueryCache; @@ -61,19 +60,18 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator { private _scoreMap: Builtins.Map | undefined; private _retainCacheCounter = 0; - constructor(builtins: Builtins) { - this.builtins = builtins; - this._cacheText = new builtins.Map(); - this._cacheQueryCSS = new builtins.Map(); - this._cacheMatches = new builtins.Map(); - this._cacheQuery = new builtins.Map(); - this._cacheMatchesSimple = new builtins.Map(); - this._cacheMatchesParents = new builtins.Map(); - this._cacheCallMatches = new builtins.Map(); - this._cacheCallQuery = new builtins.Map(); - this._cacheQuerySimple = new builtins.Map(); - - this._engines = new builtins.Map(); + constructor() { + this._cacheText = new (builtins().Map)(); + this._cacheQueryCSS = new (builtins().Map)(); + this._cacheMatches = new (builtins().Map)(); + this._cacheQuery = new (builtins().Map)(); + this._cacheMatchesSimple = new (builtins().Map)(); + this._cacheMatchesParents = new (builtins().Map)(); + this._cacheCallMatches = new (builtins().Map)(); + this._cacheCallQuery = new (builtins().Map)(); + this._cacheQuerySimple = new (builtins().Map)(); + + this._engines = new (builtins().Map)(); this._engines.set('not', notEngine); this._engines.set('is', isEngine); this._engines.set('where', isEngine); @@ -169,7 +167,7 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator { // query() recursively calls itself, so we set up a new map for this particular query() call. const previousScoreMap = this._scoreMap; - this._scoreMap = new this.builtins.Map(); + this._scoreMap = new (builtins().Map)(); let elements = this._querySimple(context, selector.simples[selector.simples.length - 1].selector); elements = elements.filter(element => this._matchesParents(element, selector, selector.simples.length - 2, context)); if (this._scoreMap.size) { @@ -398,7 +396,7 @@ const isEngine: SelectorEngine = { let elements: Element[] = []; for (const arg of args) elements = elements.concat(evaluator.query(context, arg)); - return args.length === 1 ? elements : sortInDOMOrder(evaluator.builtins, elements); + return args.length === 1 ? elements : sortInDOMOrder(elements); }, }; @@ -553,10 +551,10 @@ function previousSiblingInContext(element: Element, context: QueryContext): Elem return element.previousElementSibling || undefined; } -export function sortInDOMOrder(builtins: Builtins, elements: Iterable): Element[] { +export function sortInDOMOrder(elements: Iterable): Element[] { type SortEntry = { children: Element[], taken: boolean }; - const elementToEntry = new builtins.Map(); + const elementToEntry = new (builtins().Map)(); const roots: Element[] = []; const result: Element[] = []; @@ -583,7 +581,7 @@ export function sortInDOMOrder(builtins: Builtins, elements: Iterable): if (entry.taken) result.push(element); if (entry.children.length > 1) { - const set = new builtins.Set(entry.children); + const set = new (builtins().Set)(entry.children); entry.children = []; let child = element.firstElementChild; while (child && entry.children.length < set.size) { diff --git a/packages/injected/src/selectorGenerator.ts b/packages/injected/src/selectorGenerator.ts index 9f178768a7b2c..84650babcce04 100644 --- a/packages/injected/src/selectorGenerator.ts +++ b/packages/injected/src/selectorGenerator.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { builtins } from '@isomorphic/builtins'; import { cssEscape, escapeForAttributeSelector, escapeForTextSelector, escapeRegExp, quoteCSSAttributeValue } from '@isomorphic/stringUtils'; import { closestCrossShadow, isElementVisible, isInsideScope, parentElementOrShadowHost } from './domUtils'; @@ -77,8 +78,8 @@ export type GenerateSelectorOptions = { export function generateSelector(injectedScript: InjectedScript, targetElement: Element, options: GenerateSelectorOptions): { selector: string, selectors: string[], elements: Element[] } { injectedScript._evaluator.begin(); - const cache: Cache = { allowText: new injectedScript.builtins.Map(), disallowText: new injectedScript.builtins.Map() }; - beginAriaCaches(injectedScript.builtins); + const cache: Cache = { allowText: new (builtins().Map)(), disallowText: new (builtins().Map)() }; + beginAriaCaches(); try { let selectors: string[] = []; if (options.forTextExpect) { @@ -122,7 +123,7 @@ export function generateSelector(injectedScript: InjectedScript, targetElement: if (hasCSSIdToken(css)) tokens.push(cssFallback(injectedScript, targetElement, { ...options, noCSSId: true })); } - selectors = [...new injectedScript.builtins.Set(tokens.map(t => joinTokens(t!)))]; + selectors = [...new (builtins().Set)(tokens.map(t => joinTokens(t!)))]; } else { const targetTokens = generateSelectorFor(cache, injectedScript, targetElement, options) || cssFallback(injectedScript, targetElement, options); selectors = [joinTokens(targetTokens)]; @@ -342,7 +343,7 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i const ariaRole = getAriaRole(element); if (ariaRole && !['none', 'presentation'].includes(ariaRole)) { - const ariaName = getElementAccessibleName(injectedScript.builtins, element, false); + const ariaName = getElementAccessibleName(element, false); if (ariaName) { const roleToken = { engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: kRoleWithNameScoreExact }; candidates.push([roleToken]); diff --git a/packages/injected/src/utilityScript.ts b/packages/injected/src/utilityScript.ts index ffe60dcbc5eab..a2526f98dbf19 100644 --- a/packages/injected/src/utilityScript.ts +++ b/packages/injected/src/utilityScript.ts @@ -14,25 +14,20 @@ * limitations under the License. */ -import { ensureBuiltins } from '@isomorphic/builtins'; +import { builtins } from '@isomorphic/builtins'; import { source } from '@isomorphic/utilityScriptSerializers'; -import type { Builtins } from '@isomorphic/builtins'; - export class UtilityScript { constructor(isUnderTest: boolean) { - // eslint-disable-next-line no-restricted-globals - this.builtins = ensureBuiltins(globalThis); if (isUnderTest) { // eslint-disable-next-line no-restricted-globals - (globalThis as any).builtins = this.builtins; + (globalThis as any).builtins = builtins(); } - const result = source(this.builtins); + const result = source(builtins()); this.serializeAsCallArgument = result.serializeAsCallArgument; this.parseEvaluationResultValue = result.parseEvaluationResultValue; } - readonly builtins: Builtins; readonly serializeAsCallArgument; readonly parseEvaluationResultValue; @@ -43,7 +38,7 @@ export class UtilityScript { for (let i = 0; i < args.length; i++) parameters[i] = this.parseEvaluationResultValue(args[i], handles); - let result = this.builtins.eval(expression); + let result = builtins().eval(expression); if (isFunction === true) { result = result(...parameters); } else if (isFunction === false) { diff --git a/packages/injected/src/vueSelectorEngine.ts b/packages/injected/src/vueSelectorEngine.ts index 8d662aea97d70..e5d046576ddc2 100644 --- a/packages/injected/src/vueSelectorEngine.ts +++ b/packages/injected/src/vueSelectorEngine.ts @@ -14,12 +14,12 @@ * limitations under the License. */ +import { builtins } from '@isomorphic/builtins'; import { parseAttributeSelector } from '@isomorphic/selectorParser'; import { isInsideScope } from './domUtils'; import { matchesComponentAttribute } from './selectorUtils'; -import type { Builtins } from '@isomorphic/builtins'; import type { SelectorEngine, SelectorRoot } from './selectorEngine'; type ComponentNode = { @@ -217,11 +217,11 @@ function filterComponentsTree(treeNode: ComponentNode, searchFn: (node: Componen } type VueRoot = {version: number, root: VueVNode}; -function findVueRoots(builtins: Builtins, root: Document | ShadowRoot, roots: VueRoot[] = []): VueRoot[] { +function findVueRoots(root: Document | ShadowRoot, roots: VueRoot[] = []): VueRoot[] { const document = root.ownerDocument || root; const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); // Vue2 roots are referred to from elements. - const vue2Roots = new builtins.Set(); + const vue2Roots = new (builtins().Set)(); do { const node = walker.currentNode; if ((node as any).__vue__) @@ -231,7 +231,7 @@ function findVueRoots(builtins: Builtins, root: Document | ShadowRoot, roots: Vu roots.push({ root: (node as any)._vnode.component, version: 3 }); const shadowRoot = node instanceof Element ? node.shadowRoot : null; if (shadowRoot) - findVueRoots(builtins, shadowRoot, roots); + findVueRoots(shadowRoot, roots); } while (walker.nextNode()); for (const vue2root of vue2Roots) { roots.push({ @@ -242,11 +242,11 @@ function findVueRoots(builtins: Builtins, root: Document | ShadowRoot, roots: Vu return roots; } -export const createVueEngine = (builtins: Builtins): SelectorEngine => ({ +export const createVueEngine = (): SelectorEngine => ({ queryAll(scope: SelectorRoot, selector: string): Element[] { const document = scope.ownerDocument || scope; const { name, attributes } = parseAttributeSelector(selector, false); - const vueRoots = findVueRoots(builtins, document); + const vueRoots = findVueRoots(document); const trees = vueRoots.map(vueRoot => vueRoot.version === 3 ? buildComponentsTreeVue3(vueRoot.root) : buildComponentsTreeVue2(vueRoot.root)); const treeNodes = trees.map(tree => filterComponentsTree(tree, treeNode => { if (name && treeNode.name !== name) @@ -259,7 +259,7 @@ export const createVueEngine = (builtins: Builtins): SelectorEngine => ({ } return true; })).flat(); - const allRootElements = new builtins.Set(); + const allRootElements = new (builtins().Set)(); for (const treeNode of treeNodes) { for (const rootElement of treeNode.rootElements) allRootElements.add(rootElement); diff --git a/packages/injected/src/webSocketMock.ts b/packages/injected/src/webSocketMock.ts index 2daed84cc723d..7319e41c1b256 100644 --- a/packages/injected/src/webSocketMock.ts +++ b/packages/injected/src/webSocketMock.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ensureBuiltins } from '@isomorphic/builtins'; +import { builtins } from '@isomorphic/builtins'; export type WebSocketMessage = string | ArrayBufferLike | Blob | ArrayBufferView; export type WSData = { data: string, isBase64: boolean }; @@ -39,8 +39,6 @@ export type APIRequest = ConnectRequest | PassthroughRequest | EnsureOpenedReque type GlobalThis = typeof globalThis; export function inject(globalThis: GlobalThis) { - const builtins = ensureBuiltins(globalThis); - if ((globalThis as any).__pwWebSocketDispatch) return; @@ -91,7 +89,7 @@ export function inject(globalThis: GlobalThis) { const binding = (globalThis as any).__pwWebSocketBinding as (message: BindingPayload) => void; const NativeWebSocket: typeof WebSocket = globalThis.WebSocket; - const idToWebSocket = new builtins.Map(); + const idToWebSocket = new (builtins().Map)(); (globalThis as any).__pwWebSocketDispatch = (request: APIRequest) => { const ws = idToWebSocket.get(request.id); if (!ws) diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 14afdb74dbb28..e8e6549cd253b 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -28,7 +28,7 @@ import { mkdirIfNeeded } from './utils/fileUtils'; import { HarRecorder } from './har/harRecorder'; import { helper } from './helper'; import { SdkObject, serverSideCallMetadata } from './instrumentation'; -import { ensureBuiltins } from '../utils/isomorphic/builtins'; +import { builtins } from '../utils/isomorphic/builtins'; import * as utilityScriptSerializers from '../utils/isomorphic/utilityScriptSerializers'; import * as network from './network'; import { InitScript } from './page'; @@ -519,7 +519,7 @@ export abstract class BrowserContext extends SdkObject { }; const originsToSave = new Set(this._origins); - const collectScript = `(${storageScript.collect})(${utilityScriptSerializers.source}, (${ensureBuiltins})(globalThis), ${this._browser.options.name === 'firefox'}, ${indexedDB})`; + const collectScript = `(${storageScript.collect})(${utilityScriptSerializers.source}, (${builtins})(), ${this._browser.options.name === 'firefox'}, ${indexedDB})`; // First try collecting storage stage from existing pages. for (const page of this.pages()) { @@ -612,7 +612,7 @@ export abstract class BrowserContext extends SdkObject { for (const originState of state.origins) { const frame = page.mainFrame(); await frame.goto(metadata, originState.origin); - await frame.evaluateExpression(`(${storageScript.restore})(${utilityScriptSerializers.source}, (${ensureBuiltins})(globalThis), ${JSON.stringify(originState)})`, { world: 'utility' }); + await frame.evaluateExpression(`(${storageScript.restore})(${utilityScriptSerializers.source}, (${builtins})(), ${JSON.stringify(originState)})`, { world: 'utility' }); } await page.close(internalMetadata); } diff --git a/packages/playwright-core/src/server/javascript.ts b/packages/playwright-core/src/server/javascript.ts index 068969ea5e5b6..4413c3f19ea46 100644 --- a/packages/playwright-core/src/server/javascript.ts +++ b/packages/playwright-core/src/server/javascript.ts @@ -17,7 +17,7 @@ import { SdkObject } from './instrumentation'; import * as utilityScriptSource from '../generated/utilityScriptSource'; import { isUnderTest } from '../utils'; -import { ensureBuiltins } from '../utils/isomorphic/builtins'; +import { builtins } from '../utils/isomorphic/builtins'; import { source } from '../utils/isomorphic/utilityScriptSerializers'; import { LongStandingScope } from '../utils/isomorphic/manualPromise'; @@ -46,7 +46,7 @@ export type Func1 = string | ((arg: Unboxed) => R | Promise); export type FuncOn = string | ((on: On, arg2: Unboxed) => R | Promise); export type SmartHandle = T extends Node ? dom.ElementHandle : JSHandle; -const utilityScriptSerializers = source(ensureBuiltins(globalThis)); +const utilityScriptSerializers = source(builtins()); export const parseEvaluationResultValue = utilityScriptSerializers.parseEvaluationResultValue; export const serializeAsCallArgument = utilityScriptSerializers.serializeAsCallArgument; diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 0169c3f922d67..d10a638e6c034 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -24,7 +24,7 @@ import * as frames from './frames'; import { helper } from './helper'; import * as input from './input'; import { SdkObject } from './instrumentation'; -import { ensureBuiltins } from '../utils/isomorphic/builtins'; +import { builtins } from '../utils/isomorphic/builtins'; import { createPageBindingScript, deliverBindingResult, takeBindingHandle } from './pageBinding'; import * as js from './javascript'; import { ProgressController } from './progress'; @@ -918,7 +918,7 @@ export class InitScript { } } -export const kBuiltinsScript = new InitScript(`(${ensureBuiltins})(globalThis)`, true /* internal */); +export const kBuiltinsScript = new InitScript(`(${builtins})()`, true /* internal */); class FrameThrottler { private _acks: (() => void)[] = []; diff --git a/packages/playwright-core/src/server/pageBinding.ts b/packages/playwright-core/src/server/pageBinding.ts index 77c5013d67645..0e7f1da8d6831 100644 --- a/packages/playwright-core/src/server/pageBinding.ts +++ b/packages/playwright-core/src/server/pageBinding.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ensureBuiltins } from '../utils/isomorphic/builtins'; +import { builtins } from '../utils/isomorphic/builtins'; import { source } from '../utils/isomorphic/utilityScriptSerializers'; import type { Builtins } from '../utils/isomorphic/builtins'; @@ -88,5 +88,5 @@ export function deliverBindingResult(arg: { name: string, seq: number, result?: } export function createPageBindingScript(playwrightBinding: string, name: string, needsHandle: boolean) { - return `(${addPageBinding.toString()})(${JSON.stringify(playwrightBinding)}, ${JSON.stringify(name)}, ${needsHandle}, (${source}), (${ensureBuiltins})(globalThis))`; + return `(${addPageBinding.toString()})(${JSON.stringify(playwrightBinding)}, ${JSON.stringify(name)}, ${needsHandle}, (${source}), (${builtins})())`; } diff --git a/packages/playwright-core/src/utils/isomorphic/builtins.ts b/packages/playwright-core/src/utils/isomorphic/builtins.ts index 5126a988f842d..b95b1ff1ee223 100644 --- a/packages/playwright-core/src/utils/isomorphic/builtins.ts +++ b/packages/playwright-core/src/utils/isomorphic/builtins.ts @@ -41,7 +41,8 @@ export namespace Builtins { export type Date = OriginalDate; } -export function ensureBuiltins(global: typeof globalThis): Builtins { +export function builtins(global?: typeof globalThis): Builtins { + global = global ?? globalThis; if (!(global as any)['__playwright_builtins__']) { const builtins: Builtins = { setTimeout: global.setTimeout?.bind(global), diff --git a/tests/library/role-utils.spec.ts b/tests/library/role-utils.spec.ts index ce3b2e87bbb13..1a59094419f03 100644 --- a/tests/library/role-utils.spec.ts +++ b/tests/library/role-utils.spec.ts @@ -22,7 +22,7 @@ test.skip(({ mode }) => mode !== 'default'); async function getNameAndRole(page: Page, selector: string) { return await page.$eval(selector, e => { - const name = (window as any).__injectedScript.utils.getElementAccessibleName((window as any).__injectedScript.builtins, e); + const name = (window as any).__injectedScript.utils.getElementAccessibleName(e); const role = (window as any).__injectedScript.utils.getAriaRole(e); return { name, role }; }); @@ -92,7 +92,7 @@ for (let range = 0; range <= ranges.length; range++) { if (!element) throw new Error(`Unable to resolve "${step.selector}"`); const injected = (window as any).__injectedScript; - const received = step.property === 'name' ? injected.utils.getElementAccessibleName(injected.builtins, element) : injected.utils.getElementAccessibleDescription(injected.builtins, element); + const received = step.property === 'name' ? injected.utils.getElementAccessibleName(element) : injected.utils.getElementAccessibleDescription(element); result.push({ selector: step.selector, expected: step.value, received }); } return result; @@ -155,7 +155,7 @@ test('wpt accname non-manual', async ({ page, asset, server }) => { const injected = (window as any).__injectedScript; const title = element.getAttribute('data-testname'); const expected = element.getAttribute('data-expectedlabel'); - const received = injected.utils.getElementAccessibleName(injected.builtins, element); + const received = injected.utils.getElementAccessibleName(element); result.push({ title, expected, received }); } return result; @@ -216,7 +216,7 @@ test('axe-core accessible-text', async ({ page, asset, server }) => { const element = injected.querySelector(injected.parseSelector('css=' + selector), document, false); if (!element) throw new Error(`Unable to resolve "${selector}"`); - return injected.utils.getElementAccessibleName(injected.builtins, element); + return injected.utils.getElementAccessibleName(element); }); }, targets); expect.soft(received, `checking ${JSON.stringify(testCase)}`).toEqual(expected); From a33e9805b811743e116bf69359c74a3019509625 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 4 Apr 2025 16:01:43 +0100 Subject: [PATCH 2/3] enable for utils/isomorphic --- eslint.config.mjs | 29 +++++++++---------- packages/injected/src/clock.ts | 2 +- .../src/utils/isomorphic/builtins.ts | 2 ++ .../src/utils/isomorphic/cssParser.ts | 7 +++-- .../src/utils/isomorphic/manualPromise.ts | 9 +++++- .../src/utils/isomorphic/mimeType.ts | 5 +++- .../src/utils/isomorphic/multimap.ts | 8 +++-- .../src/utils/isomorphic/selectorParser.ts | 7 +++-- .../src/utils/isomorphic/stringUtils.ts | 8 +++-- .../src/utils/isomorphic/time.ts | 8 +++-- .../src/utils/isomorphic/timeoutRunner.ts | 9 +++--- .../src/utils/isomorphic/traceUtils.ts | 9 ++++-- .../src/utils/isomorphic/urlMatch.ts | 5 ++-- .../isomorphic/utilityScriptSerializers.ts | 3 ++ 14 files changed, 72 insertions(+), 39 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 6634d6ce31018..e7f5a8fb64c58 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -222,19 +222,19 @@ 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: "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" }, + { name: "setTimeout", message: "Use builtins().setTimeout instead" }, + { name: "clearTimeout", message: "Use builtins().clearTimeout instead" }, + { name: "setInterval", message: "Use builtins().setInterval instead" }, + { name: "clearInterval", message: "Use builtins().clearInterval instead" }, + { name: "requestAnimationFrame", message: "Use builtins().requestAnimationFrame instead" }, + { name: "cancelAnimationFrame", message: "Use builtins().cancelAnimationFrame instead" }, + { name: "requestIdleCallback", message: "Use builtins().requestIdleCallback instead" }, + { name: "cancelIdleCallback", message: "Use builtins().cancelIdleCallback instead" }, + { name: "performance", message: "Use builtins().performance instead" }, + { name: "eval", message: "Use builtins().eval instead" }, + { name: "Date", message: "Use builtins().Date instead" }, + { name: "Map", message: "Use builtins().Map instead" }, + { name: "Set", message: "Use builtins().Set instead" }, ]; const noNodeGlobalsRuleList = [{ name: "process" }]; @@ -371,8 +371,7 @@ export default [ "no-restricted-globals": [ "error", ...noNodeGlobalsRuleList, - // TODO: reenable once the violations are fixed is utils/isomorphic/*. - // ...noWebGlobalsRules, + ...noWebGlobalsRuleList, ], ...noFloatingPromisesRules, ...noBooleanCompareRules, diff --git a/packages/injected/src/clock.ts b/packages/injected/src/clock.ts index f198023302aa6..6be2421fdcd76 100644 --- a/packages/injected/src/clock.ts +++ b/packages/injected/src/clock.ts @@ -695,7 +695,7 @@ export function createClock(globalObject: WindowOrWorkerGlobalScope): { clock: C }, }; - // TODO: unify ensureBuiltins and platformOriginals + // TODO: unify Builtins and platformOriginals const clock = new ClockController(embedder); const api = createApi(clock, originals.bound); return { clock, api, originals: originals.raw }; diff --git a/packages/playwright-core/src/utils/isomorphic/builtins.ts b/packages/playwright-core/src/utils/isomorphic/builtins.ts index b95b1ff1ee223..26b720fcfe3b6 100644 --- a/packages/playwright-core/src/utils/isomorphic/builtins.ts +++ b/packages/playwright-core/src/utils/isomorphic/builtins.ts @@ -14,6 +14,8 @@ * 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'], diff --git a/packages/playwright-core/src/utils/isomorphic/cssParser.ts b/packages/playwright-core/src/utils/isomorphic/cssParser.ts index 9a791a1da4c9f..193800b802740 100644 --- a/packages/playwright-core/src/utils/isomorphic/cssParser.ts +++ b/packages/playwright-core/src/utils/isomorphic/cssParser.ts @@ -14,8 +14,11 @@ * limitations under the License. */ +import { builtins } from './builtins'; import * as css from './cssTokenizer'; +import type { Builtins } from './builtins'; + export class InvalidSelectorError extends Error { } @@ -36,7 +39,7 @@ export type CSSSimpleSelector = { css?: string, functions: CSSFunction[] }; export type CSSComplexSelector = { simples: { selector: CSSSimpleSelector, combinator: ClauseCombinator }[] }; export type CSSComplexSelectorList = CSSComplexSelector[]; -export function parseCSS(selector: string, customNames: Set): { selector: CSSComplexSelectorList, names: string[] } { +export function parseCSS(selector: string, customNames: Builtins.Set): { selector: CSSComplexSelectorList, names: string[] } { let tokens: css.CSSTokenInterface[]; try { tokens = css.tokenize(selector); @@ -71,7 +74,7 @@ export function parseCSS(selector: string, customNames: Set): { selector throw new InvalidSelectorError(`Unsupported token "${unsupportedToken.toSource()}" while parsing css selector "${selector}". Did you mean to CSS.escape it?`); let pos = 0; - const names = new Set(); + const names = new (builtins().Set)(); function unexpected() { return new InvalidSelectorError(`Unexpected token "${tokens[pos].toSource()}" while parsing css selector "${selector}". Did you mean to CSS.escape it?`); diff --git a/packages/playwright-core/src/utils/isomorphic/manualPromise.ts b/packages/playwright-core/src/utils/isomorphic/manualPromise.ts index 467b8de0dba2e..60d3ea26c3409 100644 --- a/packages/playwright-core/src/utils/isomorphic/manualPromise.ts +++ b/packages/playwright-core/src/utils/isomorphic/manualPromise.ts @@ -14,8 +14,11 @@ * limitations under the License. */ +import { builtins } from './builtins'; import { captureRawStack } from './stackTrace'; +import type { Builtins } from './builtins'; + export class ManualPromise extends Promise { private _resolve!: (t: T) => void; private _reject!: (e: Error) => void; @@ -59,9 +62,13 @@ export class ManualPromise extends Promise { export class LongStandingScope { private _terminateError: Error | undefined; private _closeError: Error | undefined; - private _terminatePromises = new Map, string[]>(); + private _terminatePromises: Builtins.Map, string[]>; private _isClosed = false; + constructor() { + this._terminatePromises = new (builtins().Map)(); + } + reject(error: Error) { this._isClosed = true; this._terminateError = error; diff --git a/packages/playwright-core/src/utils/isomorphic/mimeType.ts b/packages/playwright-core/src/utils/isomorphic/mimeType.ts index 45ac92d645c45..8157ad1c33442 100644 --- a/packages/playwright-core/src/utils/isomorphic/mimeType.ts +++ b/packages/playwright-core/src/utils/isomorphic/mimeType.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { builtins } from './builtins'; + export function isJsonMimeType(mimeType: string) { return !!mimeType.match(/^(application\/json|application\/.*?\+json|text\/(x-)?json)(;\s*charset=.*)?$/); } @@ -21,6 +23,7 @@ export function isJsonMimeType(mimeType: string) { export function isTextualMimeType(mimeType: string) { return !!mimeType.match(/^(text\/.*?|application\/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image\/svg(\+xml)?|application\/.*?(\+json|\+xml))(;\s*charset=.*)?$/); } + export function getMimeTypeForPath(path: string): string | null { const dotIndex = path.lastIndexOf('.'); if (dotIndex === -1) @@ -29,7 +32,7 @@ export function getMimeTypeForPath(path: string): string | null { return types.get(extension) || null; } -const types: Map = new Map([ +const types = new (builtins().Map)([ ['ez', 'application/andrew-inset'], ['aw', 'application/applixware'], ['atom', 'application/atom+xml'], diff --git a/packages/playwright-core/src/utils/isomorphic/multimap.ts b/packages/playwright-core/src/utils/isomorphic/multimap.ts index 18777d29e5fdd..9a45ff04af9d1 100644 --- a/packages/playwright-core/src/utils/isomorphic/multimap.ts +++ b/packages/playwright-core/src/utils/isomorphic/multimap.ts @@ -14,11 +14,15 @@ * limitations under the License. */ +import { builtins } from './builtins'; + +import type { Builtins } from './builtins'; + export class MultiMap { - private _map: Map; + private _map: Builtins.Map; constructor() { - this._map = new Map(); + this._map = new (builtins().Map)(); } set(key: K, value: V) { diff --git a/packages/playwright-core/src/utils/isomorphic/selectorParser.ts b/packages/playwright-core/src/utils/isomorphic/selectorParser.ts index f37d1ef4c31d8..779f833192cc6 100644 --- a/packages/playwright-core/src/utils/isomorphic/selectorParser.ts +++ b/packages/playwright-core/src/utils/isomorphic/selectorParser.ts @@ -14,14 +14,15 @@ * limitations under the License. */ +import { builtins } from './builtins'; import { InvalidSelectorError, parseCSS } from './cssParser'; import type { CSSComplexSelectorList } from './cssParser'; export { InvalidSelectorError, isInvalidSelectorError } from './cssParser'; export type NestedSelectorBody = { parsed: ParsedSelector, distance?: number }; -const kNestedSelectorNames = new Set(['internal:has', 'internal:has-not', 'internal:and', 'internal:or', 'internal:chain', 'left-of', 'right-of', 'above', 'below', 'near']); -const kNestedSelectorNamesWithDistance = new Set(['left-of', 'right-of', 'above', 'below', 'near']); +const kNestedSelectorNames = new (builtins().Set)(['internal:has', 'internal:has-not', 'internal:and', 'internal:or', 'internal:chain', 'left-of', 'right-of', 'above', 'below', 'near']); +const kNestedSelectorNamesWithDistance = new (builtins().Set)(['left-of', 'right-of', 'above', 'below', 'near']); export type ParsedSelectorPart = { name: string, @@ -39,7 +40,7 @@ type ParsedSelectorStrings = { capture?: number, }; -export const customCSSNames = new Set(['not', 'is', 'where', 'has', 'scope', 'light', 'visible', 'text', 'text-matches', 'text-is', 'has-text', 'above', 'below', 'right-of', 'left-of', 'near', 'nth-match']); +export const customCSSNames = new (builtins().Set)(['not', 'is', 'where', 'has', 'scope', 'light', 'visible', 'text', 'text-matches', 'text-is', 'has-text', 'above', 'below', 'right-of', 'left-of', 'near', 'nth-match']); export function parseSelector(selector: string): ParsedSelector { const parsedStrings = parseSelectorString(selector); diff --git a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts index 5a7602c97ae87..4703cf61c6a3f 100644 --- a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts +++ b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { builtins } from './builtins'; + +import type { Builtins } 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); @@ -74,10 +78,10 @@ function cssEscapeOne(s: string, i: number): string { return '\\' + s.charAt(i); } -let normalizedWhitespaceCache: Map | undefined; +let normalizedWhitespaceCache: Builtins.Map | undefined; export function cacheNormalizedWhitespaces() { - normalizedWhitespaceCache = new Map(); + normalizedWhitespaceCache = new (builtins().Map)(); } export function normalizeWhiteSpace(text: string): string { diff --git a/packages/playwright-core/src/utils/isomorphic/time.ts b/packages/playwright-core/src/utils/isomorphic/time.ts index 55cb4048be0ea..32df7eeac0d96 100644 --- a/packages/playwright-core/src/utils/isomorphic/time.ts +++ b/packages/playwright-core/src/utils/isomorphic/time.ts @@ -14,12 +14,14 @@ * limitations under the License. */ -let _timeOrigin = performance.timeOrigin; +import { builtins } from './builtins'; + +let _timeOrigin = builtins().performance.timeOrigin; let _timeShift = 0; export function setTimeOrigin(origin: number) { _timeOrigin = origin; - _timeShift = performance.timeOrigin - origin; + _timeShift = builtins().performance.timeOrigin - origin; } export function timeOrigin(): number { @@ -27,5 +29,5 @@ export function timeOrigin(): number { } export function monotonicTime(): number { - return Math.floor((performance.now() + _timeShift) * 1000) / 1000; + return Math.floor((builtins().performance.now() + _timeShift) * 1000) / 1000; } diff --git a/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts b/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts index e8016ddb49f67..29b41eaef2a2d 100644 --- a/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts +++ b/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts @@ -14,10 +14,11 @@ * limitations under the License. */ +import { builtins } from './builtins'; import { monotonicTime } from './time'; export async function raceAgainstDeadline(cb: () => Promise, deadline: number): Promise<{ result: T, timedOut: false } | { timedOut: true }> { - let timer: NodeJS.Timeout | undefined; + let timer: number | undefined; return Promise.race([ cb().then(result => { return { result, timedOut: false }; @@ -25,10 +26,10 @@ export async function raceAgainstDeadline(cb: () => Promise, deadline: num new Promise<{ timedOut: true }>(resolve => { const kMaxDeadline = 2147483647; // 2^31-1 const timeout = (deadline || kMaxDeadline) - monotonicTime(); - timer = setTimeout(() => resolve({ timedOut: true }), timeout); + timer = builtins().setTimeout(() => resolve({ timedOut: true }), timeout); }), ]).finally(() => { - clearTimeout(timer); + builtins().clearTimeout(timer); }); } @@ -49,7 +50,7 @@ export async function pollAgainstDeadline(callback: () => Promise<{ continueP const interval = pollIntervals!.shift() ?? lastPollInterval; if (deadline && deadline <= monotonicTime() + interval) break; - await new Promise(x => setTimeout(x, interval)); + await new Promise(x => builtins().setTimeout(x, interval)); } return { timedOut: true, result: lastResult }; } diff --git a/packages/playwright-core/src/utils/isomorphic/traceUtils.ts b/packages/playwright-core/src/utils/isomorphic/traceUtils.ts index f077cc5c4b4a1..fbf2e1d5202fa 100644 --- a/packages/playwright-core/src/utils/isomorphic/traceUtils.ts +++ b/packages/playwright-core/src/utils/isomorphic/traceUtils.ts @@ -14,7 +14,10 @@ * limitations under the License. */ +import { builtins } from './builtins'; + import type { ClientSideCallMetadata, StackFrame } from '@protocol/channels'; +import type { Builtins } from './builtins'; export type SerializedStackFrame = [number, number, number, string]; export type SerializedStack = [number, SerializedStackFrame[]]; @@ -24,8 +27,8 @@ export type SerializedClientSideCallMetadata = { stacks: SerializedStack[]; }; -export function parseClientSideCallMetadata(data: SerializedClientSideCallMetadata): Map { - const result = new Map(); +export function parseClientSideCallMetadata(data: SerializedClientSideCallMetadata): Builtins.Map { + const result = new (builtins().Map)(); const { files, stacks } = data; for (const s of stacks) { const [id, ff] = s; @@ -35,7 +38,7 @@ export function parseClientSideCallMetadata(data: SerializedClientSideCallMetada } export function serializeClientSideCallMetadata(metadatas: ClientSideCallMetadata[]): SerializedClientSideCallMetadata { - const fileNames = new Map(); + const fileNames = new (builtins().Map)(); const stacks: SerializedStack[] = []; for (const m of metadatas) { if (!m.stack || !m.stack.length) diff --git a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts index 35965191c9ddd..e9841f8e40c8d 100644 --- a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts +++ b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts @@ -14,10 +14,11 @@ * limitations under the License. */ +import { builtins } from './builtins'; import { isString } from './stringUtils'; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping -const escapedChars = new Set(['$', '^', '+', '.', '*', '(', ')', '|', '\\', '?', '{', '}', '[', ']']); +const escapedChars = new (builtins().Set)(['$', '^', '+', '.', '*', '(', ')', '|', '\\', '?', '{', '}', '[', ']']); export function globToRegexPattern(glob: string): string { const tokens = ['^']; @@ -118,7 +119,7 @@ function toWebSocketBaseUrl(baseURL: string | undefined) { function resolveGlobBase(baseURL: string | undefined, match: string): string { if (!match.startsWith('*')) { - const tokenMap = new Map(); + const tokenMap = new (builtins().Map)(); function mapToken(original: string, replacement: string) { if (original.length === 0) return ''; diff --git a/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts b/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts index 8e8a714f05e0d..d419c65f24dc5 100644 --- a/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts +++ b/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts @@ -178,10 +178,13 @@ export function source(builtins: Builtins) { function serialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue { if (value && typeof value === 'object') { + // eslint-disable-next-line no-restricted-globals if (typeof globalThis.Window === 'function' && value instanceof globalThis.Window) return 'ref: '; + // eslint-disable-next-line no-restricted-globals if (typeof globalThis.Document === 'function' && value instanceof globalThis.Document) return 'ref: '; + // eslint-disable-next-line no-restricted-globals if (typeof globalThis.Node === 'function' && value instanceof globalThis.Node) return 'ref: '; } From 60c31f8f3b1c84c5a76bbb644914eff40edd5ae5 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 7 Apr 2025 10:09:13 +0100 Subject: [PATCH 3/3] simplify imports even more --- eslint.config.mjs | 24 +++--- packages/injected/src/ariaSnapshot.ts | 13 ++- packages/injected/src/clock.ts | 16 ++-- packages/injected/src/highlight.ts | 8 +- packages/injected/src/injectedScript.ts | 83 +++++++++---------- packages/injected/src/reactSelectorEngine.ts | 5 +- .../injected/src/recorder/pollingRecorder.ts | 6 +- packages/injected/src/recorder/recorder.ts | 22 ++--- packages/injected/src/roleUtils.ts | 43 +++++----- packages/injected/src/selectorEvaluator.ts | 39 +++++---- packages/injected/src/selectorGenerator.ts | 11 ++- packages/injected/src/selectorUtils.ts | 8 +- packages/injected/src/vueSelectorEngine.ts | 6 +- packages/injected/src/webSocketMock.ts | 4 +- packages/playwright-core/src/server/frames.ts | 4 +- .../src/utils/isomorphic/builtins.ts | 41 +++++---- .../src/utils/isomorphic/cssParser.ts | 8 +- .../src/utils/isomorphic/manualPromise.ts | 10 +-- .../src/utils/isomorphic/mimeType.ts | 5 +- .../src/utils/isomorphic/multimap.ts | 8 +- .../src/utils/isomorphic/selectorParser.ts | 8 +- .../src/utils/isomorphic/stringUtils.ts | 8 +- .../src/utils/isomorphic/time.ts | 8 +- .../src/utils/isomorphic/timeoutRunner.ts | 8 +- .../src/utils/isomorphic/traceUtils.ts | 9 +- .../src/utils/isomorphic/urlMatch.ts | 6 +- .../isomorphic/utilityScriptSerializers.ts | 8 +- 27 files changed, 205 insertions(+), 214 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index e7f5a8fb64c58..8235961bbfa85 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -222,19 +222,19 @@ 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: "Use builtins().setTimeout instead" }, - { name: "clearTimeout", message: "Use builtins().clearTimeout instead" }, - { name: "setInterval", message: "Use builtins().setInterval instead" }, - { name: "clearInterval", message: "Use builtins().clearInterval instead" }, - { name: "requestAnimationFrame", message: "Use builtins().requestAnimationFrame instead" }, - { name: "cancelAnimationFrame", message: "Use builtins().cancelAnimationFrame instead" }, - { name: "requestIdleCallback", message: "Use builtins().requestIdleCallback instead" }, - { name: "cancelIdleCallback", message: "Use builtins().cancelIdleCallback instead" }, - { name: "performance", message: "Use builtins().performance 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: "Use builtins().Date instead" }, - { name: "Map", message: "Use builtins().Map instead" }, - { name: "Set", message: "Use builtins().Set 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" }]; diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index 021b3c86eb80d..af310747aa3b2 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { builtins } from '@isomorphic/builtins'; +import { Map, Set } from '@isomorphic/builtins'; import { escapeRegExp, longestCommonSubstring, normalizeWhiteSpace } from '@isomorphic/stringUtils'; import { getElementComputedStyle, getGlobalOptions } from './domUtils'; @@ -22,7 +22,6 @@ 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' | 'iframe'; @@ -34,19 +33,19 @@ export type AriaNode = AriaProps & { export type AriaSnapshot = { root: AriaNode; - elements: Builtins.Map; + elements: Map; generation: number; - ids: Builtins.Map; + ids: Map; }; export function generateAriaTree(rootElement: Element, generation: number): AriaSnapshot { - const visited = new (builtins().Set)(); + const visited = new Set(); const snapshot: AriaSnapshot = { root: { role: 'fragment', name: '', children: [], element: rootElement, props: {} }, - elements: new (builtins().Map)(), + elements: new Map(), generation, - ids: new (builtins().Map)(), + ids: new Map(), }; const addElement = (element: Element) => { diff --git a/packages/injected/src/clock.ts b/packages/injected/src/clock.ts index 6be2421fdcd76..cee9f2e41cae7 100644 --- a/packages/injected/src/clock.ts +++ b/packages/injected/src/clock.ts @@ -10,9 +10,7 @@ * 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 { builtins } from '@isomorphic/builtins'; - -import type { Builtins } from '@isomorphic/builtins'; +import { Map, Date } from '@isomorphic/builtins'; export type ClockMethods = { Date: DateConstructor; @@ -29,7 +27,7 @@ export type ClockMethods = { }; export type ClockConfig = { - now?: number | Builtins.Date; + now?: number | Date; }; export type InstallConfig = ClockConfig & { @@ -78,7 +76,7 @@ type LogEntryType = 'fastForward' |'install' | 'pauseAt' | 'resume' | 'runFor' | export class ClockController { readonly _now: Time; private _duringTick = false; - private _timers: Builtins.Map; + private _timers: Map; private _uniqueTimerId = idCounterStart; private _embedder: Embedder; readonly disposables: (() => void)[] = []; @@ -87,7 +85,7 @@ export class ClockController { private _currentRealTimeTimer: { callAt: Ticks, dispose: () => void } | undefined; constructor(embedder: Embedder) { - this._timers = new (builtins().Map)(); + this._timers = new Map(); this._now = { time: asWallTime(0), isFixedTime: false, ticks: 0 as Ticks, origin: asWallTime(-1) }; this._embedder = embedder; } @@ -431,7 +429,7 @@ export class ClockController { } } -function mirrorDateProperties(target: any, source: DateConstructor): DateConstructor & Builtins.Date { +function mirrorDateProperties(target: any, source: DateConstructor): DateConstructor & Date { for (const prop in source) { if (source.hasOwnProperty(prop)) target[prop] = (source as any)[prop]; @@ -445,8 +443,8 @@ function mirrorDateProperties(target: any, source: DateConstructor): DateConstru return target; } -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 { +function createDate(clock: ClockController, NativeDate: DateConstructor): DateConstructor & Date { + 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. if (!(this instanceof ClockDate)) diff --git a/packages/injected/src/highlight.ts b/packages/injected/src/highlight.ts index 20e971b6d2667..710342a29f010 100644 --- a/packages/injected/src/highlight.ts +++ b/packages/injected/src/highlight.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { builtins } from '@isomorphic/builtins'; +import { requestAnimationFrame, cancelAnimationFrame } from '@isomorphic/builtins'; import { asLocator } from '@isomorphic/locatorGenerators'; import { stringifySelector } from '@isomorphic/selectorParser'; @@ -101,7 +101,7 @@ export class Highlight { runHighlightOnRaf(selector: ParsedSelector) { if (this._rafRequest) - builtins().cancelAnimationFrame(this._rafRequest); + 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'; @@ -109,12 +109,12 @@ export class Highlight { const suffix = elements.length > 1 ? ` [${index + 1} of ${elements.length}]` : ''; return { element, color, tooltipText: locator + suffix }; })); - this._rafRequest = builtins().requestAnimationFrame(() => this.runHighlightOnRaf(selector)); + this._rafRequest = requestAnimationFrame(() => this.runHighlightOnRaf(selector)); } uninstall() { if (this._rafRequest) - builtins().cancelAnimationFrame(this._rafRequest); + cancelAnimationFrame(this._rafRequest); this._glassPaneElement.remove(); } diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index ee428d10b07df..5a1ba8bc8949e 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -15,7 +15,7 @@ */ import { parseAriaSnapshot } from '@isomorphic/ariaSnapshot'; -import { builtins } from '@isomorphic/builtins'; +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'; @@ -34,7 +34,6 @@ import { createVueEngine } from './vueSelectorEngine'; import { XPathEngine } from './xpathSelectorEngine'; import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot'; -import type { Builtins } from '@isomorphic/builtins'; import type { CSSComplexSelectorList } from '@isomorphic/cssParser'; import type { Language } from '@isomorphic/locatorGenerators'; import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '@isomorphic/selectorParser'; @@ -65,17 +64,17 @@ interface WebKitLegacyDeviceMotionEvent extends DeviceMotionEvent { } export class InjectedScript { - private _engines: Builtins.Map; + private _engines: Map; readonly _evaluator: SelectorEvaluatorImpl; private _stableRafCount: number; private _browserName: string; - readonly onGlobalListenersRemoved: Builtins.Set<() => void>; + readonly onGlobalListenersRemoved: Set<() => void>; private _hitTargetInterceptor: undefined | ((event: MouseEvent | PointerEvent | TouchEvent) => void); private _highlight: Highlight | undefined; readonly isUnderTest: boolean; private _sdkLanguage: Language; private _testIdAttributeNameForStrictErrorAndConsoleCodegen: string = 'data-testid'; - private _markedElements?: { callId: string, elements: Builtins.Set }; + private _markedElements?: { callId: string, elements: Set }; // eslint-disable-next-line no-restricted-globals readonly window: Window & typeof globalThis; readonly document: Document; @@ -95,16 +94,16 @@ export class InjectedScript { isInsideScope, normalizeWhiteSpace, parseAriaSnapshot, + builtins: builtins(), }; - readonly builtins: Builtins; - private _autoClosingTags: Builtins.Set; - private _booleanAttributes: Builtins.Set; - private _eventTypes: Builtins.Map; - private _hoverHitTargetInterceptorEvents: Builtins.Set; - private _tapHitTargetInterceptorEvents: Builtins.Set; - private _mouseHitTargetInterceptorEvents: Builtins.Set; - private _allHitTargetInterceptorEvents: Builtins.Set; + private _autoClosingTags: Set; + private _booleanAttributes: Set; + private _eventTypes: Map; + private _hoverHitTargetInterceptorEvents: Set; + private _tapHitTargetInterceptorEvents: Set; + private _mouseHitTargetInterceptorEvents: Set; + private _allHitTargetInterceptorEvents: Set; // eslint-disable-next-line no-restricted-globals constructor(window: Window & typeof globalThis, isUnderTest: boolean, sdkLanguage: Language, testIdAttributeNameForStrictErrorAndConsoleCodegen: string, stableRafCount: number, browserName: string, inputFileRoleTextbox: boolean, customEngines: { name: string, engine: SelectorEngine }[]) { @@ -113,15 +112,15 @@ export class InjectedScript { this.isUnderTest = 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.builtins = builtins(window); + this.utils.builtins = builtins(window); this._sdkLanguage = sdkLanguage; this._testIdAttributeNameForStrictErrorAndConsoleCodegen = testIdAttributeNameForStrictErrorAndConsoleCodegen; this._evaluator = new SelectorEvaluatorImpl(); - this.onGlobalListenersRemoved = new this.builtins.Set(); - this._autoClosingTags = new this.builtins.Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']); - this._booleanAttributes = new this.builtins.Set(['checked', 'selected', 'disabled', 'readonly', 'multiple']); - this._eventTypes = new this.builtins.Map([ + this.onGlobalListenersRemoved = new Set(); + this._autoClosingTags = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']); + this._booleanAttributes = new Set(['checked', 'selected', 'disabled', 'readonly', 'multiple']); + this._eventTypes = new Map([ ['auxclick', 'mouse'], ['click', 'mouse'], ['dblclick', 'mouse'], @@ -175,12 +174,12 @@ export class InjectedScript { ['devicemotion', 'devicemotion'], ]); - this._hoverHitTargetInterceptorEvents = new this.builtins.Set(['mousemove']); - this._tapHitTargetInterceptorEvents = new this.builtins.Set(['pointerdown', 'pointerup', 'touchstart', 'touchend', 'touchcancel']); - this._mouseHitTargetInterceptorEvents = new this.builtins.Set(['mousedown', 'mouseup', 'pointerdown', 'pointerup', 'click', 'auxclick', 'dblclick', 'contextmenu']); - this._allHitTargetInterceptorEvents = new this.builtins.Set([...this._hoverHitTargetInterceptorEvents, ...this._tapHitTargetInterceptorEvents, ...this._mouseHitTargetInterceptorEvents]); + this._hoverHitTargetInterceptorEvents = new Set(['mousemove']); + this._tapHitTargetInterceptorEvents = new Set(['pointerdown', 'pointerup', 'touchstart', 'touchend', 'touchcancel']); + this._mouseHitTargetInterceptorEvents = new Set(['mousedown', 'mouseup', 'pointerdown', 'pointerup', 'click', 'auxclick', 'dblclick', 'contextmenu']); + this._allHitTargetInterceptorEvents = new Set([...this._hoverHitTargetInterceptorEvents, ...this._tapHitTargetInterceptorEvents, ...this._mouseHitTargetInterceptorEvents]); - this._engines = new this.builtins.Map(); + this._engines = new Map(); this._engines.set('xpath', XPathEngine); this._engines.set('xpath:light', XPathEngine); this._engines.set('_react', createReactEngine()); @@ -260,15 +259,15 @@ export class InjectedScript { return result[0]; } - private _queryNth(elements: Builtins.Set, part: ParsedSelectorPart): Builtins.Set { + private _queryNth(elements: Set, part: ParsedSelectorPart): Set { const list = [...elements]; let nth = +part.body; if (nth === -1) nth = list.length - 1; - return new this.builtins.Set(list.slice(nth, nth + 1)); + return new Set(list.slice(nth, nth + 1)); } - private _queryLayoutSelector(elements: Builtins.Set, part: ParsedSelectorPart, originalRoot: Node): Builtins.Set { + private _queryLayoutSelector(elements: Set, part: ParsedSelectorPart, originalRoot: Node): Set { const name = part.name as LayoutSelectorName; const body = part.body as NestedSelectorBody; const result: { element: Element, score: number }[] = []; @@ -279,7 +278,7 @@ export class InjectedScript { result.push({ element, score }); } result.sort((a, b) => a.score - b.score); - return new this.builtins.Set(result.map(r => r.element)); + return new Set(result.map(r => r.element)); } ariaSnapshot(node: Node, options?: { mode?: 'raw' | 'regex', ref?: boolean }): string { @@ -327,20 +326,20 @@ export class InjectedScript { this._evaluator.begin(); try { - let roots = new this.builtins.Set([root as Element]); + let roots = new Set([root as Element]); for (const part of selector.parts) { if (part.name === 'nth') { roots = this._queryNth(roots, part); } else if (part.name === 'internal:and') { const andElements = this.querySelectorAll((part.body as NestedSelectorBody).parsed, root); - roots = new this.builtins.Set(andElements.filter(e => roots.has(e))); + roots = new Set(andElements.filter(e => roots.has(e))); } else if (part.name === 'internal:or') { const orElements = this.querySelectorAll((part.body as NestedSelectorBody).parsed, root); - roots = new this.builtins.Set(sortInDOMOrder(new this.builtins.Set([...roots, ...orElements]))); + roots = new Set(sortInDOMOrder(new Set([...roots, ...orElements]))); } else if (kLayoutSelectorNames.includes(part.name as LayoutSelectorName)) { roots = this._queryLayoutSelector(roots, part, root); } else { - const next = new this.builtins.Set(); + const next = new Set(); for (const root of roots) { const all = this._queryEngineAll(part, root); for (const one of all) @@ -544,7 +543,7 @@ export class InjectedScript { observer.observe(element); // Firefox doesn't call IntersectionObserver callback unless // there are rafs. - this.builtins.requestAnimationFrame(() => {}); + requestAnimationFrame(() => {}); }); } @@ -625,7 +624,7 @@ export class InjectedScript { return 'error:notconnected'; // Drop frames that are shorter than 16ms - WebKit Win bug. - const time = this.builtins.performance.now(); + const time = performance.now(); if (this._stableRafCount > 1 && time - lastTime < 15) return continuePolling; lastTime = time; @@ -653,12 +652,12 @@ export class InjectedScript { if (success !== continuePolling) fulfill(success); else - this.builtins.requestAnimationFrame(raf); + requestAnimationFrame(raf); } catch (e) { reject(e); } }; - this.builtins.requestAnimationFrame(raf); + requestAnimationFrame(raf); return result; } @@ -787,8 +786,8 @@ export class InjectedScript { if (element.nodeName.toLowerCase() === 'input') { const input = element as HTMLInputElement; const type = input.type.toLowerCase(); - const kInputTypesToSetValue = new this.builtins.Set(['color', 'date', 'time', 'datetime-local', 'month', 'range', 'week']); - const kInputTypesToTypeInto = new this.builtins.Set(['', 'email', 'number', 'password', 'search', 'tel', 'text', 'url']); + const kInputTypesToSetValue = new Set(['color', 'date', 'time', 'datetime-local', 'month', 'range', 'week']); + const kInputTypesToTypeInto = new Set(['', 'email', 'number', 'password', 'search', 'tel', 'text', 'url']); if (!kInputTypesToTypeInto.has(type) && !kInputTypesToSetValue.has(type)) throw this.createStacklessError(`Input of type "${type}" cannot be filled`); if (type === 'number') { @@ -1241,10 +1240,10 @@ export class InjectedScript { } } - markTargetElements(markedElements: Builtins.Set, callId: string) { + markTargetElements(markedElements: Set, callId: string) { if (this._markedElements?.callId !== callId) this._markedElements = undefined; - const previous = this._markedElements?.elements || new this.builtins.Set(); + const previous = this._markedElements?.elements || new Set(); const unmarkEvent = new CustomEvent('__playwright_unmark_target__', { bubbles: true, @@ -1466,7 +1465,7 @@ export class InjectedScript { } else if (expression === 'to.have.id') { received = element.id; } else if (expression === 'to.have.text') { - received = options.useInnerText ? (element as HTMLElement).innerText : elementText(new this.builtins.Map(), element).full; + received = options.useInnerText ? (element as HTMLElement).innerText : elementText(new Map(), element).full; } else if (expression === 'to.have.accessible.name') { received = getElementAccessibleName(element, false /* includeHidden */); } else if (expression === 'to.have.accessible.description') { @@ -1525,7 +1524,7 @@ export class InjectedScript { if (!['to.contain.text.array', 'to.have.text.array'].includes(expression)) throw this.createStacklessError('Unknown expect matcher: ' + expression); - const received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : elementText(new this.builtins.Map(), e).full); + const received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : elementText(new Map(), e).full); // "To match an array" is "to contain an array" + "equal length" const lengthShouldMatch = expression !== 'to.contain.text.array'; const matchesLength = received.length === options.expectedText.length || !lengthShouldMatch; @@ -1622,7 +1621,7 @@ class ExpectedTextMatcher { this._string = expected.matchSubstring ? undefined : this.normalize(expected.string); this._substring = expected.matchSubstring ? this.normalize(expected.string) : undefined; if (expected.regexSource) { - const flags = new (builtins().Set)((expected.regexFlags || '').split('')); + const flags = new Set((expected.regexFlags || '').split('')); if (expected.ignoreCase === false) flags.delete('i'); if (expected.ignoreCase === true) diff --git a/packages/injected/src/reactSelectorEngine.ts b/packages/injected/src/reactSelectorEngine.ts index 7d2cc5b809cb1..67df2a513ee10 100644 --- a/packages/injected/src/reactSelectorEngine.ts +++ b/packages/injected/src/reactSelectorEngine.ts @@ -14,13 +14,12 @@ * limitations under the License. */ -import { builtins } from '@isomorphic/builtins'; +import { Set } from '@isomorphic/builtins'; import { parseAttributeSelector } from '@isomorphic/selectorParser'; import { isInsideScope } from './domUtils'; import { matchesComponentAttribute } from './selectorUtils'; -import type { Builtins } from '@isomorphic/builtins'; import type { SelectorEngine, SelectorRoot } from './selectorEngine'; type ComponentNode = { @@ -216,7 +215,7 @@ export const createReactEngine = (): SelectorEngine => ({ } return true; })).flat(); - const allRootElements: Builtins.Set = new (builtins().Set)(); + const allRootElements: Set = new Set(); for (const treeNode of treeNodes) { for (const domNode of treeNode.rootElements) allRootElements.add(domNode); diff --git a/packages/injected/src/recorder/pollingRecorder.ts b/packages/injected/src/recorder/pollingRecorder.ts index d75cb1bc86ffc..c9e824951a950 100644 --- a/packages/injected/src/recorder/pollingRecorder.ts +++ b/packages/injected/src/recorder/pollingRecorder.ts @@ -54,10 +54,10 @@ export class PollingRecorder implements RecorderDelegate { private async _pollRecorderMode() { const pollPeriod = 1000; if (this._pollRecorderModeTimer) - this._recorder.injectedScript.builtins.clearTimeout(this._pollRecorderModeTimer); + this._recorder.injectedScript.utils.builtins.clearTimeout(this._pollRecorderModeTimer); const state = await this._embedder.__pw_recorderState().catch(() => null); if (!state) { - this._pollRecorderModeTimer = this._recorder.injectedScript.builtins.setTimeout(() => this._pollRecorderMode(), pollPeriod); + this._pollRecorderModeTimer = this._recorder.injectedScript.utils.builtins.setTimeout(() => this._pollRecorderMode(), pollPeriod); return; } @@ -73,7 +73,7 @@ export class PollingRecorder implements RecorderDelegate { this._recorder.setUIState(state, this); } - this._pollRecorderModeTimer = this._recorder.injectedScript.builtins.setTimeout(() => this._pollRecorderMode(), pollPeriod); + this._pollRecorderModeTimer = this._recorder.injectedScript.utils.builtins.setTimeout(() => this._pollRecorderMode(), pollPeriod); } async performAction(action: actions.PerformOnRecordAction) { diff --git a/packages/injected/src/recorder/recorder.ts b/packages/injected/src/recorder/recorder.ts index 9294d73167f81..c681ff57d63ca 100644 --- a/packages/injected/src/recorder/recorder.ts +++ b/packages/injected/src/recorder/recorder.ts @@ -23,7 +23,7 @@ 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 { Builtins } from '@isomorphic/builtins'; +import type { Set, Map } from '@isomorphic/builtins'; const HighlightColors = { multiple: '#f6b26b7f', @@ -189,7 +189,7 @@ class InspectTool implements RecorderTool { class RecordActionTool implements RecorderTool { private _recorder: Recorder; - private _performingActions: Builtins.Set; + private _performingActions: Set; private _hoveredModel: HighlightModelWithSelector | null = null; private _hoveredElement: HTMLElement | null = null; private _activeModel: HighlightModelWithSelector | null = null; @@ -198,7 +198,7 @@ class RecordActionTool implements RecorderTool { constructor(recorder: Recorder) { this._recorder = recorder; - this._performingActions = new recorder.injectedScript.builtins.Set(); + this._performingActions = new recorder.injectedScript.utils.builtins.Set(); } cursor() { @@ -252,7 +252,7 @@ class RecordActionTool implements RecorderTool { modifiers: modifiersForEvent(event), clickCount: event.detail }, - timeout: this._recorder.injectedScript.builtins.setTimeout(() => this._commitPendingClickAction(), 200) + timeout: this._recorder.injectedScript.utils.builtins.setTimeout(() => this._commitPendingClickAction(), 200) }; } } @@ -289,7 +289,7 @@ class RecordActionTool implements RecorderTool { private _cancelPendingClickAction() { if (this._pendingClickAction) - this._recorder.injectedScript.builtins.clearTimeout(this._pendingClickAction.timeout); + this._recorder.injectedScript.utils.builtins.clearTimeout(this._pendingClickAction.timeout); this._pendingClickAction = undefined; } @@ -595,12 +595,12 @@ class TextAssertionTool implements RecorderTool { private _hoverHighlight: HighlightModelWithSelector | null = null; private _action: actions.AssertAction | null = null; private _dialog: Dialog; - private _textCache: Builtins.Map; + private _textCache: Map; private _kind: 'text' | 'value' | 'snapshot'; constructor(recorder: Recorder, kind: 'text' | 'value' | 'snapshot') { this._recorder = recorder; - this._textCache = new recorder.injectedScript.builtins.Map(); + this._textCache = new recorder.injectedScript.utils.builtins.Map(); this._kind = kind; this._dialog = new Dialog(recorder); } @@ -949,7 +949,7 @@ class Overlay { else element = this._assertValuesToggle; element.classList.add('succeeded'); - this._recorder.injectedScript.builtins.setTimeout(() => element.classList.remove('succeeded'), 2000); + this._recorder.injectedScript.utils.builtins.setTimeout(() => element.classList.remove('succeeded'), 2000); } private _hideOverlay() { @@ -1084,10 +1084,10 @@ export class Recorder { let recreationInterval: number | undefined; const recreate = () => { this.highlight.install(); - recreationInterval = this.injectedScript.builtins.setTimeout(recreate, 500); + recreationInterval = this.injectedScript.utils.builtins.setTimeout(recreate, 500); }; - recreationInterval = this.injectedScript.builtins.setTimeout(recreate, 500); - this._listeners.push(() => this.injectedScript.builtins.clearTimeout(recreationInterval)); + recreationInterval = this.injectedScript.utils.builtins.setTimeout(recreate, 500); + this._listeners.push(() => this.injectedScript.utils.builtins.clearTimeout(recreationInterval)); this.highlight.appendChild(createSvgElement(this.document, clipPaths)); this.overlay?.install(); diff --git a/packages/injected/src/roleUtils.ts b/packages/injected/src/roleUtils.ts index 23e7baf1a4b38..b8c61532b69d5 100644 --- a/packages/injected/src/roleUtils.ts +++ b/packages/injected/src/roleUtils.ts @@ -14,12 +14,11 @@ * limitations under the License. */ -import { builtins } from '@isomorphic/builtins'; +import { Map, Set } from '@isomorphic/builtins'; import { getGlobalOptions, closestCrossShadow, elementSafeTagName, enclosingShadowRootOrDocument, getElementComputedStyle, isElementStyleVisibilityVisible, isVisibleTextNode, parentElementOrShadowHost } from './domUtils'; import type { AriaRole } from '@isomorphic/ariaSnapshot'; -import type { Builtins } from '@isomorphic/builtins'; function hasExplicitAccessibleName(e: Element) { return e.hasAttribute('aria-label') || e.hasAttribute('aria-labelledby'); @@ -435,7 +434,7 @@ export function getElementAccessibleName(element: Element, includeHidden: boolea // step 2. accessibleName = asFlatString(getTextAlternativeInternal(element, { includeHidden, - visitedElements: new (builtins().Set)(), + visitedElements: new Set(), embeddedInTargetElement: 'self', })); } @@ -459,7 +458,7 @@ export function getElementAccessibleDescription(element: Element, includeHidden: const describedBy = getIdRefs(element, element.getAttribute('aria-describedby')); accessibleDescription = asFlatString(describedBy.map(ref => getTextAlternativeInternal(ref, { includeHidden, - visitedElements: new (builtins().Set)(), + visitedElements: new Set(), embeddedInDescribedBy: { element: ref, hidden: isElementHiddenForAria(ref) }, })).join(' ')); } else if (element.hasAttribute('aria-description')) { @@ -519,7 +518,7 @@ export function getElementAccessibleErrorMessage(element: Element): string { // Relevant vague spec: https://w3c.github.io/core-aam/#ariaErrorMessage. const parts = errorMessages.map(errorMessage => asFlatString( getTextAlternativeInternal(errorMessage, { - visitedElements: new (builtins().Set)(), + visitedElements: new Set(), embeddedInDescribedBy: { element: errorMessage, hidden: isElementHiddenForAria(errorMessage) }, }) )); @@ -531,7 +530,7 @@ export function getElementAccessibleErrorMessage(element: Element): string { } type AccessibleNameOptions = { - visitedElements: Builtins.Set, + visitedElements: Set, includeHidden?: boolean, embeddedInDescribedBy?: { element: Element, hidden: boolean }, embeddedInLabelledBy?: { element: Element, hidden: boolean }, @@ -1054,26 +1053,26 @@ function getAccessibleNameFromAssociatedLabels(labels: Iterable !!accessibleName).join(' '); } -let cacheAccessibleName: Builtins.Map | undefined; -let cacheAccessibleNameHidden: Builtins.Map | undefined; -let cacheAccessibleDescription: Builtins.Map | undefined; -let cacheAccessibleDescriptionHidden: Builtins.Map | undefined; -let cacheAccessibleErrorMessage: Builtins.Map | undefined; -let cacheIsHidden: Builtins.Map | undefined; -let cachePseudoContentBefore: Builtins.Map | undefined; -let cachePseudoContentAfter: Builtins.Map | undefined; +let cacheAccessibleName: Map | undefined; +let cacheAccessibleNameHidden: Map | undefined; +let cacheAccessibleDescription: Map | undefined; +let cacheAccessibleDescriptionHidden: Map | undefined; +let cacheAccessibleErrorMessage: Map | undefined; +let cacheIsHidden: Map | undefined; +let cachePseudoContentBefore: Map | undefined; +let cachePseudoContentAfter: Map | undefined; let cachesCounter = 0; export function beginAriaCaches() { ++cachesCounter; - cacheAccessibleName ??= new (builtins().Map)(); - cacheAccessibleNameHidden ??= new (builtins().Map)(); - cacheAccessibleDescription ??= new (builtins().Map)(); - cacheAccessibleDescriptionHidden ??= new (builtins().Map)(); - cacheAccessibleErrorMessage ??= new (builtins().Map)(); - cacheIsHidden ??= new (builtins().Map)(); - cachePseudoContentBefore ??= new (builtins().Map)(); - cachePseudoContentAfter ??= new (builtins().Map)(); + cacheAccessibleName ??= new Map(); + cacheAccessibleNameHidden ??= new Map(); + cacheAccessibleDescription ??= new Map(); + cacheAccessibleDescriptionHidden ??= new Map(); + cacheAccessibleErrorMessage ??= new Map(); + cacheIsHidden ??= new Map(); + cachePseudoContentBefore ??= new Map(); + cachePseudoContentAfter ??= new Map(); } export function endAriaCaches() { diff --git a/packages/injected/src/selectorEvaluator.ts b/packages/injected/src/selectorEvaluator.ts index 2bb688700bcde..5350fb7267ce5 100644 --- a/packages/injected/src/selectorEvaluator.ts +++ b/packages/injected/src/selectorEvaluator.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { builtins } from '@isomorphic/builtins'; +import { Map, Set } from '@isomorphic/builtins'; import { customCSSNames } from '@isomorphic/selectorParser'; import { normalizeWhiteSpace } from '@isomorphic/stringUtils'; @@ -22,7 +22,6 @@ import { isElementVisible, parentElementOrShadowHost } from './domUtils'; import { layoutSelectorScore } from './layoutSelectorUtils'; import { elementMatchesText, elementText, shouldSkipForTextMatching } from './selectorUtils'; -import type { Builtins } from '@isomorphic/builtins'; import type { CSSComplexSelector, CSSComplexSelectorList, CSSFunctionArgument, CSSSimpleSelector } from '@isomorphic/cssParser'; import type { LayoutSelectorName } from './layoutSelectorUtils'; import type { ElementText } from './selectorUtils'; @@ -44,10 +43,10 @@ export interface SelectorEngine { query?(context: QueryContext, args: (string | number | Selector)[], evaluator: SelectorEvaluator): Element[]; } -type QueryCache = Builtins.Map; +type QueryCache = Map; export class SelectorEvaluatorImpl implements SelectorEvaluator { - private _engines: Builtins.Map; + private _engines: Map; private _cacheQueryCSS: QueryCache; private _cacheMatches: QueryCache; private _cacheQuery: QueryCache; @@ -56,22 +55,22 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator { private _cacheCallMatches: QueryCache; private _cacheCallQuery: QueryCache; private _cacheQuerySimple: QueryCache; - _cacheText: Builtins.Map; - private _scoreMap: Builtins.Map | undefined; + _cacheText: Map; + private _scoreMap: Map | undefined; private _retainCacheCounter = 0; constructor() { - this._cacheText = new (builtins().Map)(); - this._cacheQueryCSS = new (builtins().Map)(); - this._cacheMatches = new (builtins().Map)(); - this._cacheQuery = new (builtins().Map)(); - this._cacheMatchesSimple = new (builtins().Map)(); - this._cacheMatchesParents = new (builtins().Map)(); - this._cacheCallMatches = new (builtins().Map)(); - this._cacheCallQuery = new (builtins().Map)(); - this._cacheQuerySimple = new (builtins().Map)(); - - this._engines = new (builtins().Map)(); + this._cacheText = new Map(); + this._cacheQueryCSS = new Map(); + this._cacheMatches = new Map(); + this._cacheQuery = new Map(); + this._cacheMatchesSimple = new Map(); + this._cacheMatchesParents = new Map(); + this._cacheCallMatches = new Map(); + this._cacheCallQuery = new Map(); + this._cacheQuerySimple = new Map(); + + this._engines = new Map(); this._engines.set('not', notEngine); this._engines.set('is', isEngine); this._engines.set('where', isEngine); @@ -167,7 +166,7 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator { // query() recursively calls itself, so we set up a new map for this particular query() call. const previousScoreMap = this._scoreMap; - this._scoreMap = new (builtins().Map)(); + this._scoreMap = new Map(); let elements = this._querySimple(context, selector.simples[selector.simples.length - 1].selector); elements = elements.filter(element => this._matchesParents(element, selector, selector.simples.length - 2, context)); if (this._scoreMap.size) { @@ -554,7 +553,7 @@ function previousSiblingInContext(element: Element, context: QueryContext): Elem export function sortInDOMOrder(elements: Iterable): Element[] { type SortEntry = { children: Element[], taken: boolean }; - const elementToEntry = new (builtins().Map)(); + const elementToEntry = new Map(); const roots: Element[] = []; const result: Element[] = []; @@ -581,7 +580,7 @@ export function sortInDOMOrder(elements: Iterable): Element[] { if (entry.taken) result.push(element); if (entry.children.length > 1) { - const set = new (builtins().Set)(entry.children); + const set = new Set(entry.children); entry.children = []; let child = element.firstElementChild; while (child && entry.children.length < set.size) { diff --git a/packages/injected/src/selectorGenerator.ts b/packages/injected/src/selectorGenerator.ts index 84650babcce04..0e1dd16a8aa7c 100644 --- a/packages/injected/src/selectorGenerator.ts +++ b/packages/injected/src/selectorGenerator.ts @@ -14,14 +14,13 @@ * limitations under the License. */ -import { builtins } from '@isomorphic/builtins'; +import { Map, Set } from '@isomorphic/builtins'; import { cssEscape, escapeForAttributeSelector, escapeForTextSelector, escapeRegExp, quoteCSSAttributeValue } from '@isomorphic/stringUtils'; import { closestCrossShadow, isElementVisible, isInsideScope, parentElementOrShadowHost } from './domUtils'; import { beginAriaCaches, endAriaCaches, getAriaRole, getElementAccessibleName } from './roleUtils'; import { elementText, getElementLabels } from './selectorUtils'; -import type { Builtins } from '@isomorphic/builtins'; import type { InjectedScript } from './injectedScript'; type SelectorToken = { @@ -31,8 +30,8 @@ type SelectorToken = { }; type Cache = { - allowText: Builtins.Map; - disallowText: Builtins.Map; + allowText: Map; + disallowText: Map; }; const kTextScoreRange = 10; @@ -78,7 +77,7 @@ export type GenerateSelectorOptions = { export function generateSelector(injectedScript: InjectedScript, targetElement: Element, options: GenerateSelectorOptions): { selector: string, selectors: string[], elements: Element[] } { injectedScript._evaluator.begin(); - const cache: Cache = { allowText: new (builtins().Map)(), disallowText: new (builtins().Map)() }; + const cache: Cache = { allowText: new Map(), disallowText: new Map() }; beginAriaCaches(); try { let selectors: string[] = []; @@ -123,7 +122,7 @@ export function generateSelector(injectedScript: InjectedScript, targetElement: if (hasCSSIdToken(css)) tokens.push(cssFallback(injectedScript, targetElement, { ...options, noCSSId: true })); } - selectors = [...new (builtins().Set)(tokens.map(t => joinTokens(t!)))]; + selectors = [...new Set(tokens.map(t => joinTokens(t!)))]; } else { const targetTokens = generateSelectorFor(cache, injectedScript, targetElement, options) || cssFallback(injectedScript, targetElement, options); selectors = [joinTokens(targetTokens)]; diff --git a/packages/injected/src/selectorUtils.ts b/packages/injected/src/selectorUtils.ts index e7128c6049521..a2af17863fb83 100644 --- a/packages/injected/src/selectorUtils.ts +++ b/packages/injected/src/selectorUtils.ts @@ -18,7 +18,7 @@ import { normalizeWhiteSpace } from '@isomorphic/stringUtils'; import { getAriaLabelledByElements } from './roleUtils'; -import type { Builtins } from '@isomorphic/builtins'; +import type { Map } from '@isomorphic/builtins'; import type { AttributeSelectorPart } from '@isomorphic/selectorParser'; export function matchesComponentAttribute(obj: any, attr: AttributeSelectorPart) { @@ -63,7 +63,7 @@ export function shouldSkipForTextMatching(element: Element | ShadowRoot) { export type ElementText = { full: string, normalized: string, immediate: string[] }; export type TextMatcher = (text: ElementText) => boolean; -export function elementText(cache: Builtins.Map, root: Element | ShadowRoot): ElementText { +export function elementText(cache: Map, root: Element | ShadowRoot): ElementText { let value = cache.get(root); if (value === undefined) { value = { full: '', normalized: '', immediate: [] }; @@ -99,7 +99,7 @@ export function elementText(cache: Builtins.Map, element: Element, matcher: TextMatcher): 'none' | 'self' | 'selfAndChildren' { +export function elementMatchesText(cache: Map, element: Element, matcher: TextMatcher): 'none' | 'self' | 'selfAndChildren' { if (shouldSkipForTextMatching(element)) return 'none'; if (!matcher(elementText(cache, element))) @@ -113,7 +113,7 @@ export function elementMatchesText(cache: Builtins.Map, element: Element): ElementText[] { +export function getElementLabels(textCache: Map, element: Element): ElementText[] { const labels = getAriaLabelledByElements(element); if (labels) return labels.map(label => elementText(textCache, label)); diff --git a/packages/injected/src/vueSelectorEngine.ts b/packages/injected/src/vueSelectorEngine.ts index e5d046576ddc2..26caaf7c5a90e 100644 --- a/packages/injected/src/vueSelectorEngine.ts +++ b/packages/injected/src/vueSelectorEngine.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { builtins } from '@isomorphic/builtins'; +import { Set } from '@isomorphic/builtins'; import { parseAttributeSelector } from '@isomorphic/selectorParser'; import { isInsideScope } from './domUtils'; @@ -221,7 +221,7 @@ function findVueRoots(root: Document | ShadowRoot, roots: VueRoot[] = []): VueRo const document = root.ownerDocument || root; const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); // Vue2 roots are referred to from elements. - const vue2Roots = new (builtins().Set)(); + const vue2Roots = new Set(); do { const node = walker.currentNode; if ((node as any).__vue__) @@ -259,7 +259,7 @@ export const createVueEngine = (): SelectorEngine => ({ } return true; })).flat(); - const allRootElements = new (builtins().Set)(); + const allRootElements = new Set(); for (const treeNode of treeNodes) { for (const rootElement of treeNode.rootElements) allRootElements.add(rootElement); diff --git a/packages/injected/src/webSocketMock.ts b/packages/injected/src/webSocketMock.ts index 7319e41c1b256..68dcaa54ed957 100644 --- a/packages/injected/src/webSocketMock.ts +++ b/packages/injected/src/webSocketMock.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { builtins } from '@isomorphic/builtins'; +import { Map } from '@isomorphic/builtins'; export type WebSocketMessage = string | ArrayBufferLike | Blob | ArrayBufferView; export type WSData = { data: string, isBase64: boolean }; @@ -89,7 +89,7 @@ export function inject(globalThis: GlobalThis) { const binding = (globalThis as any).__pwWebSocketBinding as (message: BindingPayload) => void; const NativeWebSocket: typeof WebSocket = globalThis.WebSocket; - const idToWebSocket = new (builtins().Map)(); + const idToWebSocket = new Map(); (globalThis as any).__pwWebSocketDispatch = (request: APIRequest) => { const ws = idToWebSocket.get(request.id); if (!ws) diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 1ca9d26edb105..179a046ed41a5 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1567,9 +1567,9 @@ export class Frame extends SdkObject { return; } if (typeof polling !== 'number') - injected.builtins.requestAnimationFrame(next); + injected.utils.builtins.requestAnimationFrame(next); else - injected.builtins.setTimeout(next, polling); + injected.utils.builtins.setTimeout(next, polling); } catch (e) { reject(e); } diff --git a/packages/playwright-core/src/utils/isomorphic/builtins.ts b/packages/playwright-core/src/utils/isomorphic/builtins.ts index 26b720fcfe3b6..7a4d2502b61dd 100644 --- a/packages/playwright-core/src/utils/isomorphic/builtins.ts +++ b/packages/playwright-core/src/utils/isomorphic/builtins.ts @@ -27,22 +27,20 @@ export type Builtins = { requestIdleCallback: Window['requestIdleCallback'], cancelIdleCallback: (id: number) => void, performance: Window['performance'], - eval: typeof eval, - Intl: typeof Intl, - Date: typeof Date, - Map: typeof Map, - Set: typeof Set, + eval: typeof window['eval'], + Intl: typeof window['Intl'], + Date: typeof window['Date'], + Map: typeof window['Map'], + Set: typeof window['Set'], }; -type OriginalMap = Map; -type OriginalSet = Set; -type OriginalDate = Date; -export namespace Builtins { - export type Map = OriginalMap; - export type Set = OriginalSet; - export type Date = OriginalDate; -} - +// 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(global?: typeof globalThis): Builtins { global = global ?? globalThis; if (!(global as any)['__playwright_builtins__']) { @@ -66,3 +64,18 @@ export function builtins(global?: typeof globalThis): Builtins { } return (global as any)['__playwright_builtins__']; } + +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; diff --git a/packages/playwright-core/src/utils/isomorphic/cssParser.ts b/packages/playwright-core/src/utils/isomorphic/cssParser.ts index 193800b802740..99547830999c2 100644 --- a/packages/playwright-core/src/utils/isomorphic/cssParser.ts +++ b/packages/playwright-core/src/utils/isomorphic/cssParser.ts @@ -14,11 +14,9 @@ * limitations under the License. */ -import { builtins } from './builtins'; +import { Set } from './builtins'; import * as css from './cssTokenizer'; -import type { Builtins } from './builtins'; - export class InvalidSelectorError extends Error { } @@ -39,7 +37,7 @@ export type CSSSimpleSelector = { css?: string, functions: CSSFunction[] }; export type CSSComplexSelector = { simples: { selector: CSSSimpleSelector, combinator: ClauseCombinator }[] }; export type CSSComplexSelectorList = CSSComplexSelector[]; -export function parseCSS(selector: string, customNames: Builtins.Set): { selector: CSSComplexSelectorList, names: string[] } { +export function parseCSS(selector: string, customNames: Set): { selector: CSSComplexSelectorList, names: string[] } { let tokens: css.CSSTokenInterface[]; try { tokens = css.tokenize(selector); @@ -74,7 +72,7 @@ export function parseCSS(selector: string, customNames: Builtins.Set): { throw new InvalidSelectorError(`Unsupported token "${unsupportedToken.toSource()}" while parsing css selector "${selector}". Did you mean to CSS.escape it?`); let pos = 0; - const names = new (builtins().Set)(); + const names = new Set(); function unexpected() { return new InvalidSelectorError(`Unexpected token "${tokens[pos].toSource()}" while parsing css selector "${selector}". Did you mean to CSS.escape it?`); diff --git a/packages/playwright-core/src/utils/isomorphic/manualPromise.ts b/packages/playwright-core/src/utils/isomorphic/manualPromise.ts index 60d3ea26c3409..46498ced0962d 100644 --- a/packages/playwright-core/src/utils/isomorphic/manualPromise.ts +++ b/packages/playwright-core/src/utils/isomorphic/manualPromise.ts @@ -14,11 +14,9 @@ * limitations under the License. */ -import { builtins } from './builtins'; +import { Map } from './builtins'; import { captureRawStack } from './stackTrace'; -import type { Builtins } from './builtins'; - export class ManualPromise extends Promise { private _resolve!: (t: T) => void; private _reject!: (e: Error) => void; @@ -62,13 +60,9 @@ export class ManualPromise extends Promise { export class LongStandingScope { private _terminateError: Error | undefined; private _closeError: Error | undefined; - private _terminatePromises: Builtins.Map, string[]>; + private _terminatePromises = new Map, string[]>(); private _isClosed = false; - constructor() { - this._terminatePromises = new (builtins().Map)(); - } - reject(error: Error) { this._isClosed = true; this._terminateError = error; diff --git a/packages/playwright-core/src/utils/isomorphic/mimeType.ts b/packages/playwright-core/src/utils/isomorphic/mimeType.ts index 8157ad1c33442..ecd747a3f1de1 100644 --- a/packages/playwright-core/src/utils/isomorphic/mimeType.ts +++ b/packages/playwright-core/src/utils/isomorphic/mimeType.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { builtins } from './builtins'; +import { Map } from './builtins'; export function isJsonMimeType(mimeType: string) { return !!mimeType.match(/^(application\/json|application\/.*?\+json|text\/(x-)?json)(;\s*charset=.*)?$/); @@ -23,7 +23,6 @@ export function isJsonMimeType(mimeType: string) { export function isTextualMimeType(mimeType: string) { return !!mimeType.match(/^(text\/.*?|application\/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image\/svg(\+xml)?|application\/.*?(\+json|\+xml))(;\s*charset=.*)?$/); } - export function getMimeTypeForPath(path: string): string | null { const dotIndex = path.lastIndexOf('.'); if (dotIndex === -1) @@ -32,7 +31,7 @@ export function getMimeTypeForPath(path: string): string | null { return types.get(extension) || null; } -const types = new (builtins().Map)([ +const types: Map = new Map([ ['ez', 'application/andrew-inset'], ['aw', 'application/applixware'], ['atom', 'application/atom+xml'], diff --git a/packages/playwright-core/src/utils/isomorphic/multimap.ts b/packages/playwright-core/src/utils/isomorphic/multimap.ts index 9a45ff04af9d1..a10941be08f47 100644 --- a/packages/playwright-core/src/utils/isomorphic/multimap.ts +++ b/packages/playwright-core/src/utils/isomorphic/multimap.ts @@ -14,15 +14,13 @@ * limitations under the License. */ -import { builtins } from './builtins'; - -import type { Builtins } from './builtins'; +import { Map } from './builtins'; export class MultiMap { - private _map: Builtins.Map; + private _map: Map; constructor() { - this._map = new (builtins().Map)(); + this._map = new Map(); } set(key: K, value: V) { diff --git a/packages/playwright-core/src/utils/isomorphic/selectorParser.ts b/packages/playwright-core/src/utils/isomorphic/selectorParser.ts index 779f833192cc6..806bf781ed249 100644 --- a/packages/playwright-core/src/utils/isomorphic/selectorParser.ts +++ b/packages/playwright-core/src/utils/isomorphic/selectorParser.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import { builtins } from './builtins'; +import { Set } from './builtins'; import { InvalidSelectorError, parseCSS } from './cssParser'; import type { CSSComplexSelectorList } from './cssParser'; export { InvalidSelectorError, isInvalidSelectorError } from './cssParser'; export type NestedSelectorBody = { parsed: ParsedSelector, distance?: number }; -const kNestedSelectorNames = new (builtins().Set)(['internal:has', 'internal:has-not', 'internal:and', 'internal:or', 'internal:chain', 'left-of', 'right-of', 'above', 'below', 'near']); -const kNestedSelectorNamesWithDistance = new (builtins().Set)(['left-of', 'right-of', 'above', 'below', 'near']); +const kNestedSelectorNames = new Set(['internal:has', 'internal:has-not', 'internal:and', 'internal:or', 'internal:chain', 'left-of', 'right-of', 'above', 'below', 'near']); +const kNestedSelectorNamesWithDistance = new Set(['left-of', 'right-of', 'above', 'below', 'near']); export type ParsedSelectorPart = { name: string, @@ -40,7 +40,7 @@ type ParsedSelectorStrings = { capture?: number, }; -export const customCSSNames = new (builtins().Set)(['not', 'is', 'where', 'has', 'scope', 'light', 'visible', 'text', 'text-matches', 'text-is', 'has-text', 'above', 'below', 'right-of', 'left-of', 'near', 'nth-match']); +export const customCSSNames = new Set(['not', 'is', 'where', 'has', 'scope', 'light', 'visible', 'text', 'text-matches', 'text-is', 'has-text', 'above', 'below', 'right-of', 'left-of', 'near', 'nth-match']); export function parseSelector(selector: string): ParsedSelector { const parsedStrings = parseSelectorString(selector); diff --git a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts index 4703cf61c6a3f..b718454b71620 100644 --- a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts +++ b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import { builtins } from './builtins'; - -import type { Builtins } from './builtins'; +import { Map } from './builtins'; // NOTE: this function should not be used to escape any selectors. export function escapeWithQuotes(text: string, char: string = '\'') { @@ -78,10 +76,10 @@ function cssEscapeOne(s: string, i: number): string { return '\\' + s.charAt(i); } -let normalizedWhitespaceCache: Builtins.Map | undefined; +let normalizedWhitespaceCache: Map | undefined; export function cacheNormalizedWhitespaces() { - normalizedWhitespaceCache = new (builtins().Map)(); + normalizedWhitespaceCache = new Map(); } export function normalizeWhiteSpace(text: string): string { diff --git a/packages/playwright-core/src/utils/isomorphic/time.ts b/packages/playwright-core/src/utils/isomorphic/time.ts index 32df7eeac0d96..6e7ea7fb10534 100644 --- a/packages/playwright-core/src/utils/isomorphic/time.ts +++ b/packages/playwright-core/src/utils/isomorphic/time.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { builtins } from './builtins'; +import { performance } from './builtins'; -let _timeOrigin = builtins().performance.timeOrigin; +let _timeOrigin = performance.timeOrigin; let _timeShift = 0; export function setTimeOrigin(origin: number) { _timeOrigin = origin; - _timeShift = builtins().performance.timeOrigin - origin; + _timeShift = performance.timeOrigin - origin; } export function timeOrigin(): number { @@ -29,5 +29,5 @@ export function timeOrigin(): number { } export function monotonicTime(): number { - return Math.floor((builtins().performance.now() + _timeShift) * 1000) / 1000; + return Math.floor((performance.now() + _timeShift) * 1000) / 1000; } diff --git a/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts b/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts index 29b41eaef2a2d..ba33143a8769a 100644 --- a/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts +++ b/packages/playwright-core/src/utils/isomorphic/timeoutRunner.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { builtins } from './builtins'; +import { setTimeout, clearTimeout } from './builtins'; import { monotonicTime } from './time'; export async function raceAgainstDeadline(cb: () => Promise, deadline: number): Promise<{ result: T, timedOut: false } | { timedOut: true }> { @@ -26,10 +26,10 @@ export async function raceAgainstDeadline(cb: () => Promise, deadline: num new Promise<{ timedOut: true }>(resolve => { const kMaxDeadline = 2147483647; // 2^31-1 const timeout = (deadline || kMaxDeadline) - monotonicTime(); - timer = builtins().setTimeout(() => resolve({ timedOut: true }), timeout); + timer = setTimeout(() => resolve({ timedOut: true }), timeout); }), ]).finally(() => { - builtins().clearTimeout(timer); + clearTimeout(timer); }); } @@ -50,7 +50,7 @@ export async function pollAgainstDeadline(callback: () => Promise<{ continueP const interval = pollIntervals!.shift() ?? lastPollInterval; if (deadline && deadline <= monotonicTime() + interval) break; - await new Promise(x => builtins().setTimeout(x, interval)); + await new Promise(x => setTimeout(x, interval)); } return { timedOut: true, result: lastResult }; } diff --git a/packages/playwright-core/src/utils/isomorphic/traceUtils.ts b/packages/playwright-core/src/utils/isomorphic/traceUtils.ts index fbf2e1d5202fa..479800e3ede28 100644 --- a/packages/playwright-core/src/utils/isomorphic/traceUtils.ts +++ b/packages/playwright-core/src/utils/isomorphic/traceUtils.ts @@ -14,10 +14,9 @@ * limitations under the License. */ -import { builtins } from './builtins'; +import { Map } from './builtins'; import type { ClientSideCallMetadata, StackFrame } from '@protocol/channels'; -import type { Builtins } from './builtins'; export type SerializedStackFrame = [number, number, number, string]; export type SerializedStack = [number, SerializedStackFrame[]]; @@ -27,8 +26,8 @@ export type SerializedClientSideCallMetadata = { stacks: SerializedStack[]; }; -export function parseClientSideCallMetadata(data: SerializedClientSideCallMetadata): Builtins.Map { - const result = new (builtins().Map)(); +export function parseClientSideCallMetadata(data: SerializedClientSideCallMetadata): Map { + const result = new Map(); const { files, stacks } = data; for (const s of stacks) { const [id, ff] = s; @@ -38,7 +37,7 @@ export function parseClientSideCallMetadata(data: SerializedClientSideCallMetada } export function serializeClientSideCallMetadata(metadatas: ClientSideCallMetadata[]): SerializedClientSideCallMetadata { - const fileNames = new (builtins().Map)(); + const fileNames = new Map(); const stacks: SerializedStack[] = []; for (const m of metadatas) { if (!m.stack || !m.stack.length) diff --git a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts index e9841f8e40c8d..c267c6dd44c51 100644 --- a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts +++ b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { builtins } from './builtins'; +import { Map, Set } from './builtins'; import { isString } from './stringUtils'; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping -const escapedChars = new (builtins().Set)(['$', '^', '+', '.', '*', '(', ')', '|', '\\', '?', '{', '}', '[', ']']); +const escapedChars = new Set(['$', '^', '+', '.', '*', '(', ')', '|', '\\', '?', '{', '}', '[', ']']); export function globToRegexPattern(glob: string): string { const tokens = ['^']; @@ -119,7 +119,7 @@ function toWebSocketBaseUrl(baseURL: string | undefined) { function resolveGlobBase(baseURL: string | undefined, match: string): string { if (!match.startsWith('*')) { - const tokenMap = new (builtins().Map)(); + const tokenMap = new Map(); function mapToken(original: string, replacement: string) { if (original.length === 0) return ''; diff --git a/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts b/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts index d419c65f24dc5..209f4199b93b7 100644 --- a/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts +++ b/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Builtins } from './builtins'; +import type { Date, Map, Builtins } from './builtins'; type TypedArrayKind = 'i8' | 'ui8' | 'ui8c' | 'i16' | 'ui16' | 'i32' | 'ui32' | 'f32' | 'f64' | 'bi64' | 'bui64'; @@ -35,7 +35,7 @@ export type SerializedValue = type HandleOrValue = { h: number } | { fallThrough: any }; type VisitorInfo = { - visited: Builtins.Map; + visited: Map; lastId: number; }; @@ -49,7 +49,7 @@ export function source(builtins: Builtins) { } } - function isDate(obj: any): obj is Builtins.Date { + function isDate(obj: any): obj is Date { try { return obj instanceof builtins.Date || Object.prototype.toString.call(obj) === '[object Date]'; } catch (error) { @@ -115,7 +115,7 @@ export function source(builtins: Builtins) { return new TypedArrayConstructor(bytes.buffer); } - function parseEvaluationResultValue(value: SerializedValue, handles: any[] = [], refs: Builtins.Map = new builtins.Map()): any { + function parseEvaluationResultValue(value: SerializedValue, handles: any[] = [], refs: Map = new builtins.Map()): any { if (Object.is(value, undefined)) return undefined; if (typeof value === 'object' && value) {