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) { |   setSelectors(selectors: Selectors) { | ||||||
|     this._selectors = selectors; |     this._selectors = selectors; | ||||||
|     for (const page of this.pages()) |  | ||||||
|       page.selectors = selectors; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   selectors(): Selectors { |   selectors(): Selectors { | ||||||
|  | |||||||
| @ -95,7 +95,8 @@ export class FrameExecutionContext extends js.ExecutionContext { | |||||||
|   injectedScript(): Promise<js.JSHandle<InjectedScript>> { |   injectedScript(): Promise<js.JSHandle<InjectedScript>> { | ||||||
|     if (!this._injectedScriptPromise) { |     if (!this._injectedScriptPromise) { | ||||||
|       const custom: string[] = []; |       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}) }`); |         custom.push(`{ name: '${name}', engine: (${source}) }`); | ||||||
|       const sdkLanguage = this.frame._page.context()._browser.options.sdkLanguage; |       const sdkLanguage = this.frame._page.context()._browser.options.sdkLanguage; | ||||||
|       const source = ` |       const source = ` | ||||||
| @ -106,7 +107,7 @@ export class FrameExecutionContext extends js.ExecutionContext { | |||||||
|           globalThis, |           globalThis, | ||||||
|           ${isUnderTest()}, |           ${isUnderTest()}, | ||||||
|           "${sdkLanguage}", |           "${sdkLanguage}", | ||||||
|           ${JSON.stringify(this.frame._page.selectors.testIdAttributeName())}, |           ${JSON.stringify(selectorsRegistry.testIdAttributeName())}, | ||||||
|           ${this.frame._page._delegate.rafCountForStablePosition()}, |           ${this.frame._page._delegate.rafCountForStablePosition()}, | ||||||
|           "${this.frame._page._browserContext._browser.options.name}", |           "${this.frame._page._browserContext._browser.options.name}", | ||||||
|           [${custom.join(',\n')}] |           [${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> { |   async querySelector(selector: string, options: types.StrictOptions): Promise<ElementHandle | null> { | ||||||
|     const pair = await this._frame.resolveFrameForSelectorNoWait(selector, options, this); |     return this._frame.selectors.query(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); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async querySelectorAll(selector: string): Promise<ElementHandle<Element>[]> { |   async querySelectorAll(selector: string): Promise<ElementHandle<Element>[]> { | ||||||
|     const pair = await this._frame.resolveFrameForSelectorNoWait(selector, {}, this); |     return this._frame.selectors.queryAll(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 */); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async evalOnSelectorAndWaitForSignals(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> { |   async evalOnSelectorAndWaitForSignals(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> { | ||||||
|     const pair = await this._frame.resolveFrameForSelectorNoWait(selector, { strict }, this); |     const handle = await this._frame.selectors.query(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; |  | ||||||
|     if (!handle) |     if (!handle) | ||||||
|       throw new Error(`Error: failed to find element matching selector "${selector}"`); |       throw new Error(`Error: failed to find element matching selector "${selector}"`); | ||||||
|     const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg); |     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> { |   async evalOnSelectorAllAndWaitForSignals(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> { | ||||||
|     const pair = await this._frame.resolveFrameForSelectorNoWait(selector, {}, this); |     const arrayHandle = await this._frame.selectors.queryArrayInMainWorld(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 result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg); |     const result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg); | ||||||
|     arrayHandle.dispose(); |     arrayHandle.dispose(); | ||||||
|     return result; |     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 { serverSideCallMetadata, SdkObject } from './instrumentation'; | ||||||
| import type { InjectedScript, ElementStateWithoutStable, FrameExpectParams, InjectedScriptPoll, InjectedScriptProgress } from './injected/injectedScript'; | import type { InjectedScript, ElementStateWithoutStable, FrameExpectParams, InjectedScriptPoll, InjectedScriptProgress } from './injected/injectedScript'; | ||||||
| import { isSessionClosedError } from './protocolError'; | import { isSessionClosedError } from './protocolError'; | ||||||
| import { type ParsedSelector, isInvalidSelectorError, splitSelectorByFrame, stringifySelector } from './isomorphic/selectorParser'; | import { type ParsedSelector, isInvalidSelectorError } from './isomorphic/selectorParser'; | ||||||
| import type { SelectorInfo } from './selectors'; |  | ||||||
| import type { ScreenshotOptions } from './screenshotter'; | import type { ScreenshotOptions } from './screenshotter'; | ||||||
| import type { InputFilesItems } from './dom'; | import type { InputFilesItems } from './dom'; | ||||||
| import { asLocator } from './isomorphic/locatorGenerators'; | import { asLocator } from './isomorphic/locatorGenerators'; | ||||||
|  | import { FrameSelectors } from './frameSelectors'; | ||||||
| 
 | 
 | ||||||
| type ContextData = { | type ContextData = { | ||||||
|   contextPromise: ManualPromise<dom.FrameExecutionContext | Error>; |   contextPromise: ManualPromise<dom.FrameExecutionContext | Error>; | ||||||
| @ -91,11 +91,6 @@ export class NavigationAbortedError extends Error { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type SelectorInFrame = { |  | ||||||
|   frame: Frame; |  | ||||||
|   info: SelectorInfo; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const kDummyFrameId = '<dummy>'; | const kDummyFrameId = '<dummy>'; | ||||||
| 
 | 
 | ||||||
| export class FrameManager { | export class FrameManager { | ||||||
| @ -491,6 +486,7 @@ export class Frame extends SdkObject { | |||||||
|   private _detachedCallback = () => {}; |   private _detachedCallback = () => {}; | ||||||
|   private _raceAgainstEvaluationStallingEventsPromises = new Set<ManualPromise<any>>(); |   private _raceAgainstEvaluationStallingEventsPromises = new Set<ManualPromise<any>>(); | ||||||
|   readonly _redirectedNavigations = new Map<string, { url: string, gotoPromise: Promise<network.Response | null> }>(); // documentId -> data
 |   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) { |   constructor(page: Page, id: string, parentFrame: Frame | null) { | ||||||
|     super(page, 'frame'); |     super(page, 'frame'); | ||||||
| @ -499,6 +495,7 @@ export class Frame extends SdkObject { | |||||||
|     this._page = page; |     this._page = page; | ||||||
|     this._parentFrame = parentFrame; |     this._parentFrame = parentFrame; | ||||||
|     this._currentDocument = { documentId: undefined, request: undefined }; |     this._currentDocument = { documentId: undefined, request: undefined }; | ||||||
|  |     this.selectors = new FrameSelectors(this); | ||||||
| 
 | 
 | ||||||
|     this._detachedPromise = new Promise<void>(x => this._detachedCallback = x); |     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> { |   async querySelector(selector: string, options: types.StrictOptions): Promise<dom.ElementHandle<Element> | null> { | ||||||
|     debugLogger.log('api', `    finding element using the selector "${selector}"`); |     debugLogger.log('api', `    finding element using the selector "${selector}"`); | ||||||
|     const result = await this.resolveFrameForSelectorNoWait(selector, options); |     return this.selectors.query(selector, options); | ||||||
|     if (!result) |  | ||||||
|       return null; |  | ||||||
|     return this._page.selectors.query(result.frame, result.info); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async waitForSelector(metadata: CallMetadata, selector: string, options: types.WaitForElementOptions, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> { |   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 => { |     return controller.run(async progress => { | ||||||
|       progress.log(`waiting for ${this._asLocator(selector)}${state === 'attached' ? '' : ' to be ' + state}`); |       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 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) |         if (!resolved) | ||||||
|           return continuePolling; |           return continuePolling; | ||||||
|         const result = await resolved.injected.evaluateHandle((injected, { info, root }) => { |         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> { |   async evalOnSelectorAndWaitForSignals(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> { | ||||||
|     const pair = await this.resolveFrameForSelectorNoWait(selector, { strict }); |     const handle = await this.selectors.query(selector, { strict }); | ||||||
|     const handle = pair ? await this._page.selectors.query(pair.frame, pair.info) : null; |  | ||||||
|     if (!handle) |     if (!handle) | ||||||
|       throw new Error(`Error: failed to find element matching selector "${selector}"`); |       throw new Error(`Error: failed to find element matching selector "${selector}"`); | ||||||
|     const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg); |     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> { |   async evalOnSelectorAllAndWaitForSignals(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> { | ||||||
|     const pair = await this.resolveFrameForSelectorNoWait(selector, {}); |     const arrayHandle = await this.selectors.queryArrayInMainWorld(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 result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg); |     const result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg); | ||||||
|     arrayHandle.dispose(); |     arrayHandle.dispose(); | ||||||
|     return result; |     return result; | ||||||
| @ -867,17 +858,11 @@ export class Frame extends SdkObject { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async querySelectorAll(selector: string): Promise<dom.ElementHandle<Element>[]> { |   async querySelectorAll(selector: string): Promise<dom.ElementHandle<Element>[]> { | ||||||
|     const pair = await this.resolveFrameForSelectorNoWait(selector, {}); |     return this.selectors.queryAll(selector); | ||||||
|     if (!pair) |  | ||||||
|       return []; |  | ||||||
|     return this._page.selectors._queryAll(pair.frame, pair.info, undefined, true /* adoptToMain */); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async queryCount(selector: string): Promise<number> { |   async queryCount(selector: string): Promise<number> { | ||||||
|     const pair = await this.resolveFrameForSelectorNoWait(selector); |     return await this.selectors.queryCount(selector); | ||||||
|     if (!pair) |  | ||||||
|       throw new Error(`Error: failed to find frame for selector "${selector}"`); |  | ||||||
|     return await this._page.selectors._queryCount(pair.frame, pair.info); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async content(): Promise<string> { |   async content(): Promise<string> { | ||||||
| @ -1105,19 +1090,6 @@ export class Frame extends SdkObject { | |||||||
|     return false; |     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>( |   private async _retryWithProgressIfNotConnected<R>( | ||||||
|     progress: Progress, |     progress: Progress, | ||||||
|     selector: string, |     selector: string, | ||||||
| @ -1125,7 +1097,8 @@ export class Frame extends SdkObject { | |||||||
|     action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> { |     action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> { | ||||||
|     progress.log(`waiting for ${this._asLocator(selector)}`); |     progress.log(`waiting for ${this._asLocator(selector)}`); | ||||||
|     return this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => { |     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) |       if (!resolved) | ||||||
|         return continuePolling; |         return continuePolling; | ||||||
|       const result = await resolved.injected.evaluateHandle((injected, { info }) => { |       const result = await resolved.injected.evaluateHandle((injected, { info }) => { | ||||||
| @ -1270,14 +1243,12 @@ export class Frame extends SdkObject { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async highlight(selector: string) { |   async highlight(selector: string) { | ||||||
|     const pair = await this.resolveFrameForSelectorNoWait(selector); |     const resolved = await this.selectors.resolveInjectedForSelector(selector); | ||||||
|     if (!pair) |     if (!resolved) | ||||||
|       return; |       return; | ||||||
|     const context = await pair.frame._utilityContext(); |     return await resolved.injected.evaluate((injected, { info }) => { | ||||||
|     const injectedScript = await context.injectedScript(); |       return injected.highlight(info.parsed); | ||||||
|     return await injectedScript.evaluate((injected, { parsed }) => { |     }, { info: resolved.info }); | ||||||
|       return injected.highlight(parsed); |  | ||||||
|     }, { parsed: pair.info.parsed }); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async hideHighlight() { |   async hideHighlight() { | ||||||
| @ -1301,16 +1272,14 @@ export class Frame extends SdkObject { | |||||||
|     const controller = new ProgressController(metadata, this); |     const controller = new ProgressController(metadata, this); | ||||||
|     return controller.run(async progress => { |     return controller.run(async progress => { | ||||||
|       progress.log(`  checking visibility of ${this._asLocator(selector)}`); |       progress.log(`  checking visibility of ${this._asLocator(selector)}`); | ||||||
|       const pair = await this.resolveFrameForSelectorNoWait(selector, options); |       const resolved = await this.selectors.resolveInjectedForSelector(selector, options); | ||||||
|       if (!pair) |       if (!resolved) | ||||||
|         return false; |         return false; | ||||||
|       const context = await pair.frame._context(pair.info.world); |       return await resolved.injected.evaluate((injected, { info }) => { | ||||||
|       const injectedScript = await context.injectedScript(); |         const element = injected.querySelector(info.parsed, document, info.strict); | ||||||
|       return await injectedScript.evaluate((injected, { parsed, strict }) => { |  | ||||||
|         const element = injected.querySelector(parsed, document, strict); |  | ||||||
|         const state = element ? injected.elementState(element, 'visible') : false; |         const state = element ? injected.elementState(element, 'visible') : false; | ||||||
|         return state === 'error:notconnected' ? false : state; |         return state === 'error:notconnected' ? false : state; | ||||||
|       }, { parsed: pair.info.parsed, strict: pair.info.strict }); |       }, { info: resolved.info }); | ||||||
|     }, this._page._timeoutSettings.timeout({})); |     }, this._page._timeoutSettings.timeout({})); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -1413,7 +1382,7 @@ export class Frame extends SdkObject { | |||||||
|         progress.log(`${metadata.apiName}${timeout ? ` with timeout ${timeout}ms` : ''}`); |         progress.log(`${metadata.apiName}${timeout ? ` with timeout ${timeout}ms` : ''}`); | ||||||
|       progress.log(`waiting for ${this._asLocator(selector)}`); |       progress.log(`waiting for ${this._asLocator(selector)}`); | ||||||
|       return await this.retryWithProgressAndTimeouts(progress, [100, 250, 500, 1000], async continuePolling => { |       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(); |         progress.throwIfAborted(); | ||||||
| 
 | 
 | ||||||
|         const { frame, info } = selectorInFrame || { frame: this, info: undefined }; |         const { frame, info } = selectorInFrame || { frame: this, info: undefined }; | ||||||
| @ -1579,7 +1548,8 @@ export class Frame extends SdkObject { | |||||||
|     return controller.run(async progress => { |     return controller.run(async progress => { | ||||||
|       progress.log(`waiting for ${this._asLocator(selector)}`); |       progress.log(`waiting for ${this._asLocator(selector)}`); | ||||||
|       return this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => { |       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) |         if (!resolved) | ||||||
|           return continuePolling; |           return continuePolling; | ||||||
|         const { log, success, value } = await resolved.injected.evaluate((injected, { info, callbackText, taskData, snapshotName }) => { |         const { log, success, value } = await resolved.injected.evaluate((injected, { info, callbackText, taskData, snapshotName }) => { | ||||||
| @ -1663,32 +1633,6 @@ export class Frame extends SdkObject { | |||||||
|     }, { source, arg }); |     }, { 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) { |   async resetStorageForCurrentOriginBestEffort(newStorage: channels.OriginStorage | undefined) { | ||||||
|     const context = await this._utilityContext(); |     const context = await this._utilityContext(); | ||||||
|     await context.evaluate(async ({ ls }) => { |     await context.evaluate(async ({ ls }) => { | ||||||
|  | |||||||
| @ -36,12 +36,10 @@ import { ManualPromise } from '../utils/manualPromise'; | |||||||
| import { debugLogger } from '../common/debugLogger'; | import { debugLogger } from '../common/debugLogger'; | ||||||
| import type { ImageComparatorOptions } from '../utils/comparators'; | import type { ImageComparatorOptions } from '../utils/comparators'; | ||||||
| import { getComparator } from '../utils/comparators'; | import { getComparator } from '../utils/comparators'; | ||||||
| import type { SelectorInfo, Selectors } from './selectors'; |  | ||||||
| import type { CallMetadata } from './instrumentation'; | import type { CallMetadata } from './instrumentation'; | ||||||
| import { SdkObject } from './instrumentation'; | import { SdkObject } from './instrumentation'; | ||||||
| import type { Artifact } from './artifact'; | import type { Artifact } from './artifact'; | ||||||
| import type { TimeoutOptions } from '../common/types'; | import type { TimeoutOptions } from '../common/types'; | ||||||
| import type { ParsedSelector } from './isomorphic/selectorParser'; |  | ||||||
| import { isInvalidSelectorError } from './isomorphic/selectorParser'; | import { isInvalidSelectorError } from './isomorphic/selectorParser'; | ||||||
| import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSerializers'; | import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSerializers'; | ||||||
| import type { SerializedValue } from './isomorphic/utilityScriptSerializers'; | import type { SerializedValue } from './isomorphic/utilityScriptSerializers'; | ||||||
| @ -166,7 +164,6 @@ export class Page extends SdkObject { | |||||||
|   _clientRequestInterceptor: network.RouteHandler | undefined; |   _clientRequestInterceptor: network.RouteHandler | undefined; | ||||||
|   _serverRequestInterceptor: network.RouteHandler | undefined; |   _serverRequestInterceptor: network.RouteHandler | undefined; | ||||||
|   _ownedContext: BrowserContext | undefined; |   _ownedContext: BrowserContext | undefined; | ||||||
|   selectors: Selectors; |  | ||||||
|   _pageIsError: Error | undefined; |   _pageIsError: Error | undefined; | ||||||
|   _video: Artifact | null = null; |   _video: Artifact | null = null; | ||||||
|   _opener: Page | undefined; |   _opener: Page | undefined; | ||||||
| @ -191,7 +188,6 @@ export class Page extends SdkObject { | |||||||
|     if (delegate.pdf) |     if (delegate.pdf) | ||||||
|       this.pdf = delegate.pdf.bind(delegate); |       this.pdf = delegate.pdf.bind(delegate); | ||||||
|     this.coverage = delegate.coverage ? delegate.coverage() : null; |     this.coverage = delegate.coverage ? delegate.coverage() : null; | ||||||
|     this.selectors = browserContext.selectors(); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async initOpener(opener: PageDelegate | null) { |   async initOpener(opener: PageDelegate | null) { | ||||||
| @ -681,11 +677,6 @@ export class Page extends SdkObject { | |||||||
|     this.emit(Page.Events.PageError, error); |     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() { |   async hideHighlight() { | ||||||
|     await Promise.all(this.frames().map(frame => frame.hideHighlight().catch(() => {}))); |     await Promise.all(this.frames().map(frame => frame.hideHighlight().catch(() => {}))); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -261,7 +261,7 @@ export class Screenshotter { | |||||||
|       return cleanup; |       return cleanup; | ||||||
| 
 | 
 | ||||||
|     await Promise.all((options.mask || []).map(async ({ frame, selector }) => { |     await Promise.all((options.mask || []).map(async ({ frame, selector }) => { | ||||||
|       const pair = await frame.resolveFrameForSelectorNoWait(selector); |       const pair = await frame.selectors.resolveFrameForSelector(selector); | ||||||
|       if (pair) |       if (pair) | ||||||
|         framesToParsedSelectors.set(pair.frame, pair.info.parsed); |         framesToParsedSelectors.set(pair.frame, pair.info.parsed); | ||||||
|     })); |     })); | ||||||
|  | |||||||
| @ -14,23 +14,12 @@ | |||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| import type * as dom from './dom'; | import { allEngineNames, InvalidSelectorError, type ParsedSelector, parseSelector, stringifySelector } from './isomorphic/selectorParser'; | ||||||
| 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 { createGuid } from '../utils'; | import { createGuid } from '../utils'; | ||||||
| 
 | 
 | ||||||
| export type SelectorInfo = { |  | ||||||
|   parsed: ParsedSelector, |  | ||||||
|   world: types.World, |  | ||||||
|   strict: boolean, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export class Selectors { | export class Selectors { | ||||||
|   readonly _builtinEngines: Set<string>; |   private readonly _builtinEngines: Set<string>; | ||||||
|   readonly _builtinEnginesInMainWorld: Set<string>; |   private readonly _builtinEnginesInMainWorld: Set<string>; | ||||||
|   readonly _engines: Map<string, { source: string, contentScript: boolean }>; |   readonly _engines: Map<string, { source: string, contentScript: boolean }>; | ||||||
|   readonly guid = `selectors@${createGuid()}`; |   readonly guid = `selectors@${createGuid()}`; | ||||||
|   private _testIdAttributeName: string = 'data-testid'; |   private _testIdAttributeName: string = 'data-testid'; | ||||||
| @ -78,72 +67,7 @@ export class Selectors { | |||||||
|     this._engines.clear(); |     this._engines.clear(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async query(frame: frames.Frame, info: SelectorInfo, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> { |   parseSelector(selector: string | ParsedSelector, strict: boolean) { | ||||||
|     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 { |  | ||||||
|     const parsed = typeof selector === 'string' ? parseSelector(selector) : selector; |     const parsed = typeof selector === 'string' ? parseSelector(selector) : selector; | ||||||
|     let needsMainWorld = false; |     let needsMainWorld = false; | ||||||
|     for (const name of allEngineNames(parsed)) { |     for (const name of allEngineNames(parsed)) { | ||||||
| @ -157,7 +81,7 @@ export class Selectors { | |||||||
|     } |     } | ||||||
|     return { |     return { | ||||||
|       parsed, |       parsed, | ||||||
|       world: needsMainWorld ? 'main' : 'utility', |       world: needsMainWorld ? 'main' as const : 'utility' as const, | ||||||
|       strict, |       strict, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dmitry Gozman
						Dmitry Gozman