mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: split off FrameSelectors helper class (#21042)
This class manages everything related to querying selector for the frame.
This commit is contained in:
parent
ce692830b3
commit
c69a7424b4
@ -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 {
|
||||
|
@ -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;
|
||||
|
156
packages/playwright-core/src/server/frameSelectors.ts
Normal file
156
packages/playwright-core/src/server/frameSelectors.ts
Normal 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;
|
||||
}
|
@ -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 }) => {
|
||||
|
@ -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(() => {})));
|
||||
}
|
||||
|
@ -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);
|
||||
}));
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user