chore: split off FrameSelectors helper class (#21042)

This class manages everything related to querying selector for the
frame.
This commit is contained in:
Dmitry Gozman 2023-02-21 14:08:51 -08:00 committed by GitHub
parent ce692830b3
commit c69a7424b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 195 additions and 198 deletions

View File

@ -102,8 +102,6 @@ export abstract class BrowserContext extends SdkObject {
setSelectors(selectors: Selectors) {
this._selectors = selectors;
for (const page of this.pages())
page.selectors = selectors;
}
selectors(): Selectors {

View File

@ -95,7 +95,8 @@ export class FrameExecutionContext extends js.ExecutionContext {
injectedScript(): Promise<js.JSHandle<InjectedScript>> {
if (!this._injectedScriptPromise) {
const custom: string[] = [];
for (const [name, { source }] of this.frame._page.selectors._engines)
const selectorsRegistry = this.frame._page.context().selectors();
for (const [name, { source }] of selectorsRegistry._engines)
custom.push(`{ name: '${name}', engine: (${source}) }`);
const sdkLanguage = this.frame._page.context()._browser.options.sdkLanguage;
const source = `
@ -106,7 +107,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
globalThis,
${isUnderTest()},
"${sdkLanguage}",
${JSON.stringify(this.frame._page.selectors.testIdAttributeName())},
${JSON.stringify(selectorsRegistry.testIdAttributeName())},
${this.frame._page._delegate.rafCountForStablePosition()},
"${this.frame._page._browserContext._browser.options.name}",
[${custom.join(',\n')}]
@ -754,27 +755,15 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async querySelector(selector: string, options: types.StrictOptions): Promise<ElementHandle | null> {
const pair = await this._frame.resolveFrameForSelectorNoWait(selector, options, this);
if (!pair)
return null;
const { frame, info } = pair;
// If we end up in the same frame => use the scope again, line above was noop.
return this._page.selectors.query(frame, info, this._frame === frame ? this : undefined);
return this._frame.selectors.query(selector, options, this);
}
async querySelectorAll(selector: string): Promise<ElementHandle<Element>[]> {
const pair = await this._frame.resolveFrameForSelectorNoWait(selector, {}, this);
if (!pair)
return [];
const { frame, info } = pair;
// If we end up in the same frame => use the scope again, line above was noop.
return this._page.selectors._queryAll(frame, info, this._frame === frame ? this : undefined, true /* adoptToMain */);
return this._frame.selectors.queryAll(selector, this);
}
async evalOnSelectorAndWaitForSignals(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
const pair = await this._frame.resolveFrameForSelectorNoWait(selector, { strict }, this);
// If we end up in the same frame => use the scope again, line above was noop.
const handle = pair ? await this._page.selectors.query(pair.frame, pair.info, this._frame === pair.frame ? this : undefined) : null;
const handle = await this._frame.selectors.query(selector, { strict }, this);
if (!handle)
throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
@ -783,12 +772,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async evalOnSelectorAllAndWaitForSignals(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
const pair = await this._frame.resolveFrameForSelectorNoWait(selector, {}, this);
if (!pair)
throw new Error(`Error: failed to find frame for selector "${selector}"`);
const { frame, info } = pair;
// If we end up in the same frame => use the scope again, line above was noop.
const arrayHandle = await this._page.selectors._queryArrayInMainWorld(frame, info, this._frame === frame ? this : undefined);
const arrayHandle = await this._frame.selectors.queryArrayInMainWorld(selector, this);
const result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
arrayHandle.dispose();
return result;

View File

@ -0,0 +1,156 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type Frame } from './frames';
import type * as types from './types';
import { stringifySelector, type ParsedSelector, splitSelectorByFrame } from './isomorphic/selectorParser';
import { type FrameExecutionContext, type ElementHandle } from './dom';
import { type JSHandle } from './javascript';
import { type InjectedScript } from './injected/injectedScript';
export type SelectorInfo = {
parsed: ParsedSelector,
world: types.World,
strict: boolean,
};
export type SelectorInFrame = {
frame: Frame;
info: SelectorInfo;
scope?: ElementHandle;
};
export class FrameSelectors {
readonly frame: Frame;
constructor(frame: Frame) {
this.frame = frame;
}
private _parseSelector(selector: string | ParsedSelector, options?: types.StrictOptions): SelectorInfo {
const strict = typeof options?.strict === 'boolean' ? options.strict : !!this.frame._page.context()._options.strictSelectors;
return this.frame._page.context().selectors().parseSelector(selector, strict);
}
async query(selector: string, options?: types.StrictOptions, scope?: ElementHandle): Promise<ElementHandle<Element> | null> {
const resolved = await this.resolveInjectedForSelector(selector, options, scope);
// Be careful, |this.frame| can be different from |resolved.frame|.
if (!resolved)
return null;
const handle = await resolved.injected.evaluateHandle((injected, { info, scope }) => {
return injected.querySelector(info.parsed, scope || document, info.strict);
}, { info: resolved.info, scope: resolved.scope });
const elementHandle = handle.asElement() as ElementHandle<Element> | null;
if (!elementHandle) {
handle.dispose();
return null;
}
return adoptIfNeeded(elementHandle, await resolved.frame._mainContext());
}
async queryArrayInMainWorld(selector: string, scope?: ElementHandle): Promise<JSHandle<Element[]>> {
const resolved = await this.resolveInjectedForSelector(selector, { mainWorld: true }, scope);
// Be careful, |this.frame| can be different from |resolved.frame|.
if (!resolved)
throw new Error(`Error: failed to find frame for selector "${selector}"`);
return await resolved.injected.evaluateHandle((injected, { info, scope }) => {
return injected.querySelectorAll(info.parsed, scope || document);
}, { info: resolved.info, scope: resolved.scope });
}
async queryCount(selector: string): Promise<number> {
const resolved = await this.resolveInjectedForSelector(selector);
// Be careful, |this.frame| can be different from |resolved.frame|.
if (!resolved)
throw new Error(`Error: failed to find frame for selector "${selector}"`);
return await resolved.injected.evaluate((injected, { info }) => {
return injected.querySelectorAll(info.parsed, document).length;
}, { info: resolved.info });
}
async queryAll(selector: string, scope?: ElementHandle): Promise<ElementHandle<Element>[]> {
const resolved = await this.resolveInjectedForSelector(selector, {}, scope);
// Be careful, |this.frame| can be different from |resolved.frame|.
if (!resolved)
return [];
const arrayHandle = await resolved.injected.evaluateHandle((injected, { info, scope }) => {
return injected.querySelectorAll(info.parsed, scope || document);
}, { info: resolved.info, scope: resolved.scope });
const properties = await arrayHandle.getProperties();
arrayHandle.dispose();
// Note: adopting elements one by one may be slow. If we encounter the issue here,
// we might introduce 'useMainContext' option or similar to speed things up.
const targetContext = await resolved.frame._mainContext();
const result: Promise<ElementHandle<Element>>[] = [];
for (const property of properties.values()) {
const elementHandle = property.asElement() as ElementHandle<Element>;
if (elementHandle)
result.push(adoptIfNeeded(elementHandle, targetContext));
else
property.dispose();
}
return Promise.all(result);
}
async resolveFrameForSelector(selector: string, options: types.StrictOptions = {}, scope?: ElementHandle): Promise<SelectorInFrame | null> {
let frame: Frame = this.frame;
const frameChunks = splitSelectorByFrame(selector);
for (let i = 0; i < frameChunks.length - 1; ++i) {
const info = this._parseSelector(frameChunks[i], options);
const context = await frame._context(info.world);
const injectedScript = await context.injectedScript();
const handle = await injectedScript.evaluateHandle((injected, { info, scope, selectorString }) => {
const element = injected.querySelector(info.parsed, scope || document, info.strict);
if (element && element.nodeName !== 'IFRAME' && element.nodeName !== 'FRAME')
throw injected.createStacklessError(`Selector "${selectorString}" resolved to ${injected.previewNode(element)}, <iframe> was expected`);
return element;
}, { info, scope: i === 0 ? scope : undefined, selectorString: stringifySelector(info.parsed) });
const element = handle.asElement() as ElementHandle<Element> | null;
if (!element)
return null;
const maybeFrame = await frame._page._delegate.getContentFrame(element);
element.dispose();
if (!maybeFrame)
return null;
frame = maybeFrame;
}
// If we end up in the different frame, we should start from the frame root, so throw away the scope.
if (frame !== this.frame)
scope = undefined;
return { frame, info: frame.selectors._parseSelector(frameChunks[frameChunks.length - 1], options), scope };
}
async resolveInjectedForSelector(selector: string, options?: { strict?: boolean, mainWorld?: boolean }, scope?: ElementHandle): Promise<{ injected: JSHandle<InjectedScript>, info: SelectorInfo, frame: Frame, scope?: ElementHandle } | undefined> {
const resolved = await this.resolveFrameForSelector(selector, options, scope);
// Be careful, |this.frame| can be different from |resolved.frame|.
if (!resolved)
return;
const context = await resolved.frame._context(options?.mainWorld ? 'main' : resolved.info.world);
const injected = await context.injectedScript();
return { injected, info: resolved.info, frame: resolved.frame, scope: resolved.scope };
}
}
async function adoptIfNeeded<T extends Node>(handle: ElementHandle<T>, context: FrameExecutionContext): Promise<ElementHandle<T>> {
if (handle._context === context)
return handle;
const adopted = handle._page._delegate.adoptElementHandle(handle, context);
handle.dispose();
return adopted;
}

View File

@ -36,11 +36,11 @@ import type { CallMetadata } from './instrumentation';
import { serverSideCallMetadata, SdkObject } from './instrumentation';
import type { InjectedScript, ElementStateWithoutStable, FrameExpectParams, InjectedScriptPoll, InjectedScriptProgress } from './injected/injectedScript';
import { isSessionClosedError } from './protocolError';
import { type ParsedSelector, isInvalidSelectorError, splitSelectorByFrame, stringifySelector } from './isomorphic/selectorParser';
import type { SelectorInfo } from './selectors';
import { type ParsedSelector, isInvalidSelectorError } from './isomorphic/selectorParser';
import type { ScreenshotOptions } from './screenshotter';
import type { InputFilesItems } from './dom';
import { asLocator } from './isomorphic/locatorGenerators';
import { FrameSelectors } from './frameSelectors';
type ContextData = {
contextPromise: ManualPromise<dom.FrameExecutionContext | Error>;
@ -91,11 +91,6 @@ export class NavigationAbortedError extends Error {
}
}
type SelectorInFrame = {
frame: Frame;
info: SelectorInfo;
};
const kDummyFrameId = '<dummy>';
export class FrameManager {
@ -491,6 +486,7 @@ export class Frame extends SdkObject {
private _detachedCallback = () => {};
private _raceAgainstEvaluationStallingEventsPromises = new Set<ManualPromise<any>>();
readonly _redirectedNavigations = new Map<string, { url: string, gotoPromise: Promise<network.Response | null> }>(); // documentId -> data
readonly selectors: FrameSelectors;
constructor(page: Page, id: string, parentFrame: Frame | null) {
super(page, 'frame');
@ -499,6 +495,7 @@ export class Frame extends SdkObject {
this._page = page;
this._parentFrame = parentFrame;
this._currentDocument = { documentId: undefined, request: undefined };
this.selectors = new FrameSelectors(this);
this._detachedPromise = new Promise<void>(x => this._detachedCallback = x);
@ -773,10 +770,7 @@ export class Frame extends SdkObject {
async querySelector(selector: string, options: types.StrictOptions): Promise<dom.ElementHandle<Element> | null> {
debugLogger.log('api', ` finding element using the selector "${selector}"`);
const result = await this.resolveFrameForSelectorNoWait(selector, options);
if (!result)
return null;
return this._page.selectors.query(result.frame, result.info);
return this.selectors.query(selector, options);
}
async waitForSelector(metadata: CallMetadata, selector: string, options: types.WaitForElementOptions, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
@ -791,7 +785,8 @@ export class Frame extends SdkObject {
return controller.run(async progress => {
progress.log(`waiting for ${this._asLocator(selector)}${state === 'attached' ? '' : ' to be ' + state}`);
const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
const resolved = await this._resolveInjectedForSelector(progress, selector, options, scope);
const resolved = await this.selectors.resolveInjectedForSelector(selector, options, scope);
progress.throwIfAborted();
if (!resolved)
return continuePolling;
const result = await resolved.injected.evaluateHandle((injected, { info, root }) => {
@ -839,8 +834,7 @@ export class Frame extends SdkObject {
}
async evalOnSelectorAndWaitForSignals(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
const pair = await this.resolveFrameForSelectorNoWait(selector, { strict });
const handle = pair ? await this._page.selectors.query(pair.frame, pair.info) : null;
const handle = await this.selectors.query(selector, { strict });
if (!handle)
throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
@ -849,10 +843,7 @@ export class Frame extends SdkObject {
}
async evalOnSelectorAllAndWaitForSignals(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
const pair = await this.resolveFrameForSelectorNoWait(selector, {});
if (!pair)
throw new Error(`Error: failed to find frame for selector "${selector}"`);
const arrayHandle = await this._page.selectors._queryArrayInMainWorld(pair.frame, pair.info);
const arrayHandle = await this.selectors.queryArrayInMainWorld(selector);
const result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
arrayHandle.dispose();
return result;
@ -867,17 +858,11 @@ export class Frame extends SdkObject {
}
async querySelectorAll(selector: string): Promise<dom.ElementHandle<Element>[]> {
const pair = await this.resolveFrameForSelectorNoWait(selector, {});
if (!pair)
return [];
return this._page.selectors._queryAll(pair.frame, pair.info, undefined, true /* adoptToMain */);
return this.selectors.queryAll(selector);
}
async queryCount(selector: string): Promise<number> {
const pair = await this.resolveFrameForSelectorNoWait(selector);
if (!pair)
throw new Error(`Error: failed to find frame for selector "${selector}"`);
return await this._page.selectors._queryCount(pair.frame, pair.info);
return await this.selectors.queryCount(selector);
}
async content(): Promise<string> {
@ -1105,19 +1090,6 @@ export class Frame extends SdkObject {
return false;
}
private async _resolveInjectedForSelector(progress: Progress, selector: string, options: { strict?: boolean, mainWorld?: boolean }, scope?: dom.ElementHandle): Promise<{ injected: js.JSHandle<InjectedScript>, info: SelectorInfo, frame: Frame } | undefined> {
const selectorInFrame = await this.resolveFrameForSelectorNoWait(selector, options, scope);
if (!selectorInFrame)
return;
progress.throwIfAborted();
// Be careful, |this| can be different from |selectorInFrame.frame|.
const context = await selectorInFrame.frame._context(options.mainWorld ? 'main' : selectorInFrame.info.world);
const injected = await context.injectedScript();
progress.throwIfAborted();
return { injected, info: selectorInFrame.info, frame: selectorInFrame.frame };
}
private async _retryWithProgressIfNotConnected<R>(
progress: Progress,
selector: string,
@ -1125,7 +1097,8 @@ export class Frame extends SdkObject {
action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> {
progress.log(`waiting for ${this._asLocator(selector)}`);
return this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
const resolved = await this._resolveInjectedForSelector(progress, selector, { strict });
const resolved = await this.selectors.resolveInjectedForSelector(selector, { strict });
progress.throwIfAborted();
if (!resolved)
return continuePolling;
const result = await resolved.injected.evaluateHandle((injected, { info }) => {
@ -1270,14 +1243,12 @@ export class Frame extends SdkObject {
}
async highlight(selector: string) {
const pair = await this.resolveFrameForSelectorNoWait(selector);
if (!pair)
const resolved = await this.selectors.resolveInjectedForSelector(selector);
if (!resolved)
return;
const context = await pair.frame._utilityContext();
const injectedScript = await context.injectedScript();
return await injectedScript.evaluate((injected, { parsed }) => {
return injected.highlight(parsed);
}, { parsed: pair.info.parsed });
return await resolved.injected.evaluate((injected, { info }) => {
return injected.highlight(info.parsed);
}, { info: resolved.info });
}
async hideHighlight() {
@ -1301,16 +1272,14 @@ export class Frame extends SdkObject {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
progress.log(` checking visibility of ${this._asLocator(selector)}`);
const pair = await this.resolveFrameForSelectorNoWait(selector, options);
if (!pair)
const resolved = await this.selectors.resolveInjectedForSelector(selector, options);
if (!resolved)
return false;
const context = await pair.frame._context(pair.info.world);
const injectedScript = await context.injectedScript();
return await injectedScript.evaluate((injected, { parsed, strict }) => {
const element = injected.querySelector(parsed, document, strict);
return await resolved.injected.evaluate((injected, { info }) => {
const element = injected.querySelector(info.parsed, document, info.strict);
const state = element ? injected.elementState(element, 'visible') : false;
return state === 'error:notconnected' ? false : state;
}, { parsed: pair.info.parsed, strict: pair.info.strict });
}, { info: resolved.info });
}, this._page._timeoutSettings.timeout({}));
}
@ -1413,7 +1382,7 @@ export class Frame extends SdkObject {
progress.log(`${metadata.apiName}${timeout ? ` with timeout ${timeout}ms` : ''}`);
progress.log(`waiting for ${this._asLocator(selector)}`);
return await this.retryWithProgressAndTimeouts(progress, [100, 250, 500, 1000], async continuePolling => {
const selectorInFrame = await this.resolveFrameForSelectorNoWait(selector, { strict: true });
const selectorInFrame = await this.selectors.resolveFrameForSelector(selector, { strict: true });
progress.throwIfAborted();
const { frame, info } = selectorInFrame || { frame: this, info: undefined };
@ -1579,7 +1548,8 @@ export class Frame extends SdkObject {
return controller.run(async progress => {
progress.log(`waiting for ${this._asLocator(selector)}`);
return this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
const resolved = await this._resolveInjectedForSelector(progress, selector, options);
const resolved = await this.selectors.resolveInjectedForSelector(selector, options);
progress.throwIfAborted();
if (!resolved)
return continuePolling;
const { log, success, value } = await resolved.injected.evaluate((injected, { info, callbackText, taskData, snapshotName }) => {
@ -1663,32 +1633,6 @@ export class Frame extends SdkObject {
}, { source, arg });
}
async resolveFrameForSelectorNoWait(selector: string, options: types.StrictOptions = {}, scope?: dom.ElementHandle): Promise<SelectorInFrame | null> {
let frame: Frame = this;
const frameChunks = splitSelectorByFrame(selector);
for (let i = 0; i < frameChunks.length - 1; ++i) {
const info = this._page.parseSelector(frameChunks[i], options);
const context = await frame._context(info.world);
const injectedScript = await context.injectedScript();
const handle = await injectedScript.evaluateHandle((injected, { info, scope, selectorString }) => {
const element = injected.querySelector(info.parsed, scope || document, info.strict);
if (element && element.nodeName !== 'IFRAME' && element.nodeName !== 'FRAME')
throw injected.createStacklessError(`Selector "${selectorString}" resolved to ${injected.previewNode(element)}, <iframe> was expected`);
return element;
}, { info, scope: i === 0 ? scope : undefined, selectorString: stringifySelector(info.parsed) });
const element = handle.asElement() as dom.ElementHandle<Element> | null;
if (!element)
return null;
const maybeFrame = await this._page._delegate.getContentFrame(element);
element.dispose();
if (!maybeFrame)
return null;
frame = maybeFrame;
}
return { frame, info: this._page.parseSelector(frameChunks[frameChunks.length - 1], options) };
}
async resetStorageForCurrentOriginBestEffort(newStorage: channels.OriginStorage | undefined) {
const context = await this._utilityContext();
await context.evaluate(async ({ ls }) => {

View File

@ -36,12 +36,10 @@ import { ManualPromise } from '../utils/manualPromise';
import { debugLogger } from '../common/debugLogger';
import type { ImageComparatorOptions } from '../utils/comparators';
import { getComparator } from '../utils/comparators';
import type { SelectorInfo, Selectors } from './selectors';
import type { CallMetadata } from './instrumentation';
import { SdkObject } from './instrumentation';
import type { Artifact } from './artifact';
import type { TimeoutOptions } from '../common/types';
import type { ParsedSelector } from './isomorphic/selectorParser';
import { isInvalidSelectorError } from './isomorphic/selectorParser';
import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSerializers';
import type { SerializedValue } from './isomorphic/utilityScriptSerializers';
@ -166,7 +164,6 @@ export class Page extends SdkObject {
_clientRequestInterceptor: network.RouteHandler | undefined;
_serverRequestInterceptor: network.RouteHandler | undefined;
_ownedContext: BrowserContext | undefined;
selectors: Selectors;
_pageIsError: Error | undefined;
_video: Artifact | null = null;
_opener: Page | undefined;
@ -191,7 +188,6 @@ export class Page extends SdkObject {
if (delegate.pdf)
this.pdf = delegate.pdf.bind(delegate);
this.coverage = delegate.coverage ? delegate.coverage() : null;
this.selectors = browserContext.selectors();
}
async initOpener(opener: PageDelegate | null) {
@ -681,11 +677,6 @@ export class Page extends SdkObject {
this.emit(Page.Events.PageError, error);
}
parseSelector(selector: string | ParsedSelector, options?: types.StrictOptions): SelectorInfo {
const strict = typeof options?.strict === 'boolean' ? options.strict : !!this.context()._options.strictSelectors;
return this.selectors.parseSelector(selector, strict);
}
async hideHighlight() {
await Promise.all(this.frames().map(frame => frame.hideHighlight().catch(() => {})));
}

View File

@ -261,7 +261,7 @@ export class Screenshotter {
return cleanup;
await Promise.all((options.mask || []).map(async ({ frame, selector }) => {
const pair = await frame.resolveFrameForSelectorNoWait(selector);
const pair = await frame.selectors.resolveFrameForSelector(selector);
if (pair)
framesToParsedSelectors.set(pair.frame, pair.info.parsed);
}));

View File

@ -14,23 +14,12 @@
* limitations under the License.
*/
import type * as dom from './dom';
import type * as frames from './frames';
import type * as js from './javascript';
import type * as types from './types';
import type { ParsedSelector } from './isomorphic/selectorParser';
import { allEngineNames, InvalidSelectorError, parseSelector, stringifySelector } from './isomorphic/selectorParser';
import { allEngineNames, InvalidSelectorError, type ParsedSelector, parseSelector, stringifySelector } from './isomorphic/selectorParser';
import { createGuid } from '../utils';
export type SelectorInfo = {
parsed: ParsedSelector,
world: types.World,
strict: boolean,
};
export class Selectors {
readonly _builtinEngines: Set<string>;
readonly _builtinEnginesInMainWorld: Set<string>;
private readonly _builtinEngines: Set<string>;
private readonly _builtinEnginesInMainWorld: Set<string>;
readonly _engines: Map<string, { source: string, contentScript: boolean }>;
readonly guid = `selectors@${createGuid()}`;
private _testIdAttributeName: string = 'data-testid';
@ -78,72 +67,7 @@ export class Selectors {
this._engines.clear();
}
async query(frame: frames.Frame, info: SelectorInfo, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
const context = await frame._context(info.world);
const injectedScript = await context.injectedScript();
const handle = await injectedScript.evaluateHandle((injected, { parsed, scope, strict }) => {
return injected.querySelector(parsed, scope || document, strict);
}, { parsed: info.parsed, scope, strict: info.strict });
const elementHandle = handle.asElement() as dom.ElementHandle<Element> | null;
if (!elementHandle) {
handle.dispose();
return null;
}
const mainContext = await frame._mainContext();
return this._adoptIfNeeded(elementHandle, mainContext);
}
async _queryArrayInMainWorld(frame: frames.Frame, info: SelectorInfo, scope?: dom.ElementHandle): Promise<js.JSHandle<Element[]>> {
const context = await frame._mainContext();
const injectedScript = await context.injectedScript();
const arrayHandle = await injectedScript.evaluateHandle((injected, { parsed, scope }) => {
return injected.querySelectorAll(parsed, scope || document);
}, { parsed: info.parsed, scope });
return arrayHandle;
}
async _queryCount(frame: frames.Frame, info: SelectorInfo, scope?: dom.ElementHandle): Promise<number> {
const context = await frame._context(info.world);
const injectedScript = await context.injectedScript();
return await injectedScript.evaluate((injected, { parsed, scope }) => {
return injected.querySelectorAll(parsed, scope || document).length;
}, { parsed: info.parsed, scope });
}
async _queryAll(frame: frames.Frame, selector: SelectorInfo, scope?: dom.ElementHandle, adoptToMain?: boolean): Promise<dom.ElementHandle<Element>[]> {
const info = typeof selector === 'string' ? frame._page.parseSelector(selector) : selector;
const context = await frame._context(info.world);
const injectedScript = await context.injectedScript();
const arrayHandle = await injectedScript.evaluateHandle((injected, { parsed, scope }) => {
return injected.querySelectorAll(parsed, scope || document);
}, { parsed: info.parsed, scope });
const properties = await arrayHandle.getProperties();
arrayHandle.dispose();
// Note: adopting elements one by one may be slow. If we encounter the issue here,
// we might introduce 'useMainContext' option or similar to speed things up.
const targetContext = adoptToMain ? await frame._mainContext() : context;
const result: Promise<dom.ElementHandle<Element>>[] = [];
for (const property of properties.values()) {
const elementHandle = property.asElement() as dom.ElementHandle<Element>;
if (elementHandle)
result.push(this._adoptIfNeeded(elementHandle, targetContext));
else
property.dispose();
}
return Promise.all(result);
}
private async _adoptIfNeeded<T extends Node>(handle: dom.ElementHandle<T>, context: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
if (handle._context === context)
return handle;
const adopted = handle._page._delegate.adoptElementHandle(handle, context);
handle.dispose();
return adopted;
}
parseSelector(selector: string | ParsedSelector, strict: boolean): SelectorInfo {
parseSelector(selector: string | ParsedSelector, strict: boolean) {
const parsed = typeof selector === 'string' ? parseSelector(selector) : selector;
let needsMainWorld = false;
for (const name of allEngineNames(parsed)) {
@ -157,7 +81,7 @@ export class Selectors {
}
return {
parsed,
world: needsMainWorld ? 'main' : 'utility',
world: needsMainWorld ? 'main' as const : 'utility' as const,
strict,
};
}