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
	 Dmitry Gozman
						Dmitry Gozman