From 0876b99a4dbeb4ea101f37b3ba8a353a0113b077 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 8 May 2025 15:18:28 -0700 Subject: [PATCH] chore: upstream the frame tree snapshot (#35902) --- .../playwright-core/src/client/locator.ts | 9 +++- .../playwright-core/src/protocol/debug.ts | 1 + .../playwright-core/src/protocol/validator.ts | 7 ++- .../src/server/dispatchers/frameDispatcher.ts | 4 ++ packages/playwright-core/src/server/dom.ts | 2 +- packages/playwright-core/src/server/frames.ts | 32 ++++++++++++- packages/protocol/src/channels.d.ts | 12 ++++- packages/protocol/src/protocol.yml | 9 +++- tests/page/page-aria-snapshot-ai.spec.ts | 47 ++++++------------- 9 files changed, 81 insertions(+), 42 deletions(-) diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index be42ffc60e..0dd097a498 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -303,8 +303,13 @@ export class Locator implements api.Locator { return await this._withElement((h, timeout) => h.screenshot({ ...options, mask, timeout }), options.timeout); } - async ariaSnapshot(options?: { _forAI?: boolean } & TimeoutOptions): Promise { - const result = await this._frame._channel.ariaSnapshot({ ...options, forAI: options?._forAI, selector: this._selector }); + async ariaSnapshot(options?: TimeoutOptions): Promise { + const result = await this._frame._channel.ariaSnapshot({ ...options, selector: this._selector }); + return result.snapshot; + } + + async _snapshotForAI(): Promise { + const result = await this._frame._channel.snapshotForAI({ selector: this._selector }); return result.snapshot; } diff --git a/packages/playwright-core/src/protocol/debug.ts b/packages/playwright-core/src/protocol/debug.ts index c58c3e4aaf..58b1282a16 100644 --- a/packages/playwright-core/src/protocol/debug.ts +++ b/packages/playwright-core/src/protocol/debug.ts @@ -95,6 +95,7 @@ export const commandsWithTracingSnapshots = new Set([ 'Frame.addScriptTag', 'Frame.addStyleTag', 'Frame.ariaSnapshot', + 'Frame.snapshotForAI', 'Frame.blur', 'Frame.check', 'Frame.click', diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 361742b8f6..5b870ad841 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1484,12 +1484,17 @@ scheme.FrameAddStyleTagResult = tObject({ }); scheme.FrameAriaSnapshotParams = tObject({ selector: tString, - forAI: tOptional(tBoolean), timeout: tOptional(tNumber), }); scheme.FrameAriaSnapshotResult = tObject({ snapshot: tString, }); +scheme.FrameSnapshotForAIParams = tObject({ + selector: tString, +}); +scheme.FrameSnapshotForAIResult = tObject({ + snapshot: tString, +}); scheme.FrameBlurParams = tObject({ selector: tString, strict: tOptional(tBoolean), diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index ef8b891d2f..e87a8bfd8f 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -271,4 +271,8 @@ export class FrameDispatcher extends Dispatcher { return { snapshot: await this._frame.ariaSnapshot(metadata, params.selector, params) }; } + + async snapshotForAI(params: channels.FrameSnapshotForAIParams, metadata: CallMetadata): Promise { + return { snapshot: await this._frame.snapshotForAI(metadata, params.selector) }; + } } diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index a047680092..331611108a 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -811,7 +811,7 @@ export class ElementHandle extends js.JSHandle { return this._page.delegate.getBoundingBox(this); } - async ariaSnapshot(options: { forAI?: boolean }): Promise { + async ariaSnapshot(options?: { forAI?: boolean }): Promise { return await this.evaluateInUtility(([injected, element, options]) => injected.ariaSnapshot(element, options), options); } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index b45bba2fb8..937c452031 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1411,13 +1411,41 @@ export class Frame extends SdkObject { }); } - async ariaSnapshot(metadata: CallMetadata, selector: string, options: { forAI?: boolean } & types.TimeoutOptions = {}): Promise { + async ariaSnapshot(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise { const controller = new ProgressController(metadata, this); return controller.run(async progress => { - return await this._retryWithProgressIfNotConnected(progress, selector, true /* strict */, true /* performActionPreChecks */, handle => handle.ariaSnapshot(options)); + return await this._retryWithProgressIfNotConnected(progress, selector, true /* strict */, true /* performActionPreChecks */, handle => handle.ariaSnapshot()); }, this._page.timeoutSettings.timeout(options)); } + async snapshotForAI(metadata: CallMetadata, selector: string): Promise { + const allFrameSnapshot = async (selector: string): Promise => { + const controller = new ProgressController(metadata, this); + const snapshot = await controller.run(async progress => { + return await this._retryWithProgressIfNotConnected(progress, selector, true /* strict */, true /* performActionPreChecks */, handle => handle.ariaSnapshot({ forAI: true })); + }, this._page.timeoutSettings.defaultTimeout()); + + const lines = snapshot.split('\n'); + const result = []; + for (const line of lines) { + const match = line.match(/^(\s*)- iframe \[ref=(.*)\]/); + if (!match) { + result.push(line); + continue; + } + + const leadingSpace = match[1]; + const ref = match[2]; + const childSelector = `${selector} >> aria-ref=${ref} >> internal:control=enter-frame >> body`; + const childSnapshot = await allFrameSnapshot(childSelector); + result.push(line + ':', childSnapshot.split('\n').map(l => leadingSpace + ' ' + l).join('\n')); + } + return result.join('\n'); + }; + + return await allFrameSnapshot(selector); + } + async expect(metadata: CallMetadata, selector: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> { const result = await this._expectImpl(metadata, selector, options); // Library mode special case for the expect errors which are return values, not exceptions. diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 0b7fa280e6..97913011b4 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -2589,6 +2589,7 @@ export interface FrameChannel extends FrameEventTarget, Channel { addScriptTag(params: FrameAddScriptTagParams, metadata?: CallMetadata): Promise; addStyleTag(params: FrameAddStyleTagParams, metadata?: CallMetadata): Promise; ariaSnapshot(params: FrameAriaSnapshotParams, metadata?: CallMetadata): Promise; + snapshotForAI(params: FrameSnapshotForAIParams, metadata?: CallMetadata): Promise; blur(params: FrameBlurParams, metadata?: CallMetadata): Promise; check(params: FrameCheckParams, metadata?: CallMetadata): Promise; click(params: FrameClickParams, metadata?: CallMetadata): Promise; @@ -2695,16 +2696,23 @@ export type FrameAddStyleTagResult = { }; export type FrameAriaSnapshotParams = { selector: string, - forAI?: boolean, timeout?: number, }; export type FrameAriaSnapshotOptions = { - forAI?: boolean, timeout?: number, }; export type FrameAriaSnapshotResult = { snapshot: string, }; +export type FrameSnapshotForAIParams = { + selector: string, +}; +export type FrameSnapshotForAIOptions = { + +}; +export type FrameSnapshotForAIResult = { + snapshot: string, +}; export type FrameBlurParams = { selector: string, strict?: boolean, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 94553efd79..d3ca7190c5 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1976,13 +1976,20 @@ Frame: ariaSnapshot: parameters: selector: string - forAI: boolean? timeout: number? returns: snapshot: string flags: snapshot: true + snapshotForAI: + parameters: + selector: string + returns: + snapshot: string + flags: + snapshot: true + blur: parameters: selector: string diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 16872d2b5f..b1b060fccc 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import type { FrameLocator, Page } from '@playwright/test'; import { test as it, expect } from './pageTest'; -const forAI = { _forAI: true } as any; +function snapshotForAI(page: any): Promise { + return page.locator('body')._snapshotForAI(); +} it('should generate refs', async ({ page }) => { await page.setContent(` @@ -26,7 +27,7 @@ it('should generate refs', async ({ page }) => { `); - const snapshot1 = await page.locator('body').ariaSnapshot(forAI); + const snapshot1 = await snapshotForAI(page); expect(snapshot1).toContainYaml(` - generic [ref=e1]: - button "One" [ref=e2] @@ -41,7 +42,7 @@ it('should generate refs', async ({ page }) => { e.textContent = 'Not Two'; }); - const snapshot2 = await page.locator('body').ariaSnapshot(forAI); + const snapshot2 = await snapshotForAI(page); expect(snapshot2).toContainYaml(` - generic [ref=e1]: - button "One" [ref=e2] @@ -56,37 +57,17 @@ it('should list iframes', async ({ page }) => {