diff --git a/packages/playwright-core/src/client/elementHandle.ts b/packages/playwright-core/src/client/elementHandle.ts index 85c91e8c22..93a600336d 100644 --- a/packages/playwright-core/src/client/elementHandle.ts +++ b/packages/playwright-core/src/client/elementHandle.ts @@ -61,11 +61,6 @@ export class ElementHandle extends JSHandle implements return Frame.fromNullable((await this._elementChannel.contentFrame()).frame); } - async _generateLocatorString(): Promise { - const value = (await this._elementChannel.generateLocatorString()).value; - return value === undefined ? null : value; - } - async getAttribute(name: string): Promise { const value = (await this._elementChannel.getAttribute({ name })).value; return value === undefined ? null : value; diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index ea780c2fbe..b52c99e922 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -250,7 +250,8 @@ export class Locator implements api.Locator { } async _generateLocatorString(): Promise { - return await this._withElement(h => h._generateLocatorString(), { title: 'Generate locator string', internal: true }); + const { value } = await this._frame._channel.generateLocatorString({ selector: this._selector }); + return value === undefined ? null : value; } async getAttribute(name: string, options?: TimeoutOptions): Promise { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 2475526a43..a44bb2d096 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1651,6 +1651,12 @@ scheme.FrameFrameElementParams = tOptional(tObject({})); scheme.FrameFrameElementResult = tObject({ element: tChannel(['ElementHandle']), }); +scheme.FrameGenerateLocatorStringParams = tObject({ + selector: tString, +}); +scheme.FrameGenerateLocatorStringResult = tObject({ + value: tOptional(tString), +}); scheme.FrameHighlightParams = tObject({ selector: tString, }); @@ -2044,10 +2050,6 @@ scheme.ElementHandleFillParams = tObject({ scheme.ElementHandleFillResult = tOptional(tObject({})); scheme.ElementHandleFocusParams = tOptional(tObject({})); scheme.ElementHandleFocusResult = tOptional(tObject({})); -scheme.ElementHandleGenerateLocatorStringParams = tOptional(tObject({})); -scheme.ElementHandleGenerateLocatorStringResult = tObject({ - value: tOptional(tString), -}); scheme.ElementHandleGetAttributeParams = tObject({ name: tString, }); diff --git a/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts b/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts index c9c50829a9..4f70827048 100644 --- a/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts @@ -65,10 +65,6 @@ export class ElementHandleDispatcher extends JSHandleDispatcher return { frame: frame ? FrameDispatcher.from(this._browserContextDispatcher(), frame) : undefined }; } - async generateLocatorString(params: channels.ElementHandleGenerateLocatorStringParams, metadata: CallMetadata): Promise { - return { value: await this._elementHandle.generateLocatorString() }; - } - async getAttribute(params: channels.ElementHandleGetAttributeParams, metadata: CallMetadata): Promise { const value = await this._elementHandle.getAttribute(metadata, params.name); return { value: value === null ? undefined : value }; diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index 38c31056f2..5db8c7c561 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -178,6 +178,10 @@ export class FrameDispatcher extends Dispatcher { + return { value: await this._frame.generateLocatorString(metadata, params.selector) }; + } + async getAttribute(params: channels.FrameGetAttributeParams, metadata: CallMetadata): Promise { const value = await this._frame.getAttribute(metadata, params.selector, params.name, params); return { value: value === null ? undefined : value }; diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index f1f234864d..ffb61584fc 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -18,7 +18,7 @@ import fs from 'fs'; import * as js from './javascript'; import { ProgressController } from './progress'; -import { asLocator, isUnderTest } from '../utils'; +import { isUnderTest } from '../utils'; import { prepareFilesForUpload } from './fileUploadUtils'; import * as rawInjectedScriptSource from '../generated/injectedScriptSource'; @@ -183,38 +183,6 @@ export class ElementHandle extends js.JSHandle { return this._page.delegate.getContentFrame(this); } - async generateLocatorString(): Promise { - const selectors = await this._generateSelectorString(); - if (!selectors.length) - return; - return asLocator('javascript', selectors.reverse().join(' >> internal:control=enter-frame >> ')); - } - - private async _generateSelectorString(): Promise { - const selector = await this.evaluateInUtility(async ([injected, node]) => { - return injected.generateSelectorSimple(node as unknown as Element); - }, {}); - if (selector === 'error:notconnected') - return []; - - let frame: frames.Frame | null = this._frame; - const result: string[] = [selector]; - while (frame?.parentFrame()) { - const frameElement = await frame.frameElement(); - if (frameElement) { - const selector = await frameElement.evaluateInUtility(async ([injected, node]) => { - return injected.generateSelectorSimple(node as unknown as Element); - }, {}); - frameElement.dispose(); - if (selector === 'error:notconnected') - return []; - result.push(selector); - } - frame = frame.parentFrame(); - } - return result; - } - async getAttribute(metadata: CallMetadata, name: string): Promise { return this._frame.getAttribute(metadata, ':scope', name, { timeout: 0 }, this); } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 33e738ac7d..ecb5757c39 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1212,6 +1212,38 @@ export class Frame extends SdkObject { }, options.timeout); } + async generateLocatorString(metadata: CallMetadata, selector: string): Promise { + const controller = new ProgressController(metadata, this); + return controller.run(async progress => { + const element = await progress.race(this.selectors.query(selector)); + if (!element) + throw new Error(`No element matching ${this._asLocator(selector)}`); + + const generated = await progress.race(element.evaluateInUtility(async ([injected, node]) => { + return injected.generateSelectorSimple(node as unknown as Element); + }, {})); + if (!generated) + throw new Error(`Unable to generate locator for ${this._asLocator(selector)}`); + + let frame: Frame | null = element._frame; + const result = [generated]; + while (frame?.parentFrame()) { + const frameElement = await progress.race(frame.frameElement()); + if (frameElement) { + const generated = await progress.race(frameElement.evaluateInUtility(async ([injected, node]) => { + return injected.generateSelectorSimple(node as unknown as Element); + }, {})); + frameElement.dispose(); + if (generated === 'error:notconnected' || !generated) + throw new Error(`Unable to generate locator for ${this._asLocator(selector)}`); + result.push(generated); + } + frame = frame.parentFrame(); + } + return asLocator(this._page.browserContext._browser.sdkLanguage(), result.reverse().join(' >> internal:control=enter-frame >> ')); + }); + } + async textContent(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions, scope?: dom.ElementHandle): Promise { return this._callOnElementOnceMatches(metadata, selector, (injected, element) => element.textContent, undefined, options, scope); } diff --git a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts index 673dcd2612..2075f076dd 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts @@ -154,6 +154,7 @@ export const methodMetainfo = new Map; focus(params: FrameFocusParams, metadata?: CallMetadata): Promise; frameElement(params?: FrameFrameElementParams, metadata?: CallMetadata): Promise; + generateLocatorString(params: FrameGenerateLocatorStringParams, metadata?: CallMetadata): Promise; highlight(params: FrameHighlightParams, metadata?: CallMetadata): Promise; getAttribute(params: FrameGetAttributeParams, metadata?: CallMetadata): Promise; goto(params: FrameGotoParams, metadata?: CallMetadata): Promise; @@ -2890,6 +2891,15 @@ export type FrameFrameElementOptions = {}; export type FrameFrameElementResult = { element: ElementHandleChannel, }; +export type FrameGenerateLocatorStringParams = { + selector: string, +}; +export type FrameGenerateLocatorStringOptions = { + +}; +export type FrameGenerateLocatorStringResult = { + value?: string, +}; export type FrameHighlightParams = { selector: string, }; @@ -3395,7 +3405,6 @@ export interface ElementHandleChannel extends ElementHandleEventTarget, JSHandle dispatchEvent(params: ElementHandleDispatchEventParams, metadata?: CallMetadata): Promise; fill(params: ElementHandleFillParams, metadata?: CallMetadata): Promise; focus(params?: ElementHandleFocusParams, metadata?: CallMetadata): Promise; - generateLocatorString(params?: ElementHandleGenerateLocatorStringParams, metadata?: CallMetadata): Promise; getAttribute(params: ElementHandleGetAttributeParams, metadata?: CallMetadata): Promise; hover(params: ElementHandleHoverParams, metadata?: CallMetadata): Promise; innerHTML(params?: ElementHandleInnerHTMLParams, metadata?: CallMetadata): Promise; @@ -3531,11 +3540,6 @@ export type ElementHandleFillResult = void; export type ElementHandleFocusParams = {}; export type ElementHandleFocusOptions = {}; export type ElementHandleFocusResult = void; -export type ElementHandleGenerateLocatorStringParams = {}; -export type ElementHandleGenerateLocatorStringOptions = {}; -export type ElementHandleGenerateLocatorStringResult = { - value?: string, -}; export type ElementHandleGetAttributeParams = { name: string, }; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index e9c765d045..be56d9af82 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2289,6 +2289,13 @@ Frame: returns: element: ElementHandle + generateLocatorString: + internal: true + parameters: + selector: string + returns: + value: string? + highlight: internal: true parameters: @@ -2937,11 +2944,6 @@ ElementHandle: slowMo: true snapshot: true - generateLocatorString: - internal: true - returns: - value: string? - getAttribute: internal: true parameters: diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 35d591bf9f..3d12cd086f 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -101,6 +101,10 @@ it('should stitch all frame snapshots', async ({ page, server }) => { const locator = await (page.locator('aria-ref=f2e2').describe('foo bar') as any)._generateLocatorString(); expect(locator).toBe(`locator('iframe[name=\"2frames\"]').contentFrame().locator('iframe[name=\"uno\"]').contentFrame().getByText('Hi, I\\'m frame')`); } + { + const error = await (page.locator('aria-ref=e1000') as any)._generateLocatorString().catch(e => e); + expect(error.message).toContain(`No element matching locator('aria-ref=e1000')`); + } }); it('should not generate refs for elements with pointer-events:none', async ({ page }) => {