From f0a031c31acfdf45ee5bfc7b3d5dc9cfb632089a Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 18 Mar 2026 14:53:29 -0700 Subject: [PATCH] chore: simplify snapshotForAI to return string with mode option Push the full/incremental decision into snapshotForAI so callers get a single string back instead of a {full, incremental?} tuple. Add mode: 'full' | 'incremental' parameter to control which snapshot is returned while preserving tracking state. --- docs/src/api/class-locator.md | 3 +- docs/src/api/class-page.md | 16 +++++++--- packages/playwright-client/types/types.d.ts | 31 +++++++------------ .../playwright-core/src/client/locator.ts | 5 +-- packages/playwright-core/src/client/page.ts | 5 +-- .../playwright-core/src/protocol/validator.ts | 4 +-- packages/playwright-core/src/server/page.ts | 7 +++-- .../src/tools/backend/response.ts | 4 +-- .../playwright-core/src/tools/backend/tab.ts | 11 +++---- packages/playwright-core/types/types.d.ts | 31 +++++++------------ packages/playwright/src/index.ts | 2 +- .../playwright/src/mcp/test/browserBackend.ts | 2 +- packages/protocol/src/channels.d.ts | 5 +-- packages/protocol/src/protocol.yml | 8 +++-- tests/page/page-aria-snapshot-ai.spec.ts | 15 ++++++--- 15 files changed, 74 insertions(+), 75 deletions(-) diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index d361b0b1a2f29..c8f3570acd0f7 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -2487,8 +2487,7 @@ This method expects [Locator] to point to an ## async method: Locator.snapshotForAI * since: v1.59 -- returns: <[Object]> - - `full` <[string]> Accessibility snapshot of the element matching this locator. +- returns: <[string]> Returns an accessibility snapshot of the element's subtree optimized for AI consumption. diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 730ced1bad463..9978466b1bd9c 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -4213,9 +4213,7 @@ Page height in pixels. ## async method: Page.snapshotForAI * since: v1.59 -- returns: <[Object]> - - `full` <[string]> Full accessibility snapshot of the page. - - `incremental` ?<[string]> Incremental snapshot containing only changes since the last tracked snapshot, when using the [`option: Page.snapshotForAI.track`] option. +- returns: <[string]> Returns an accessibility snapshot of the page optimized for AI consumption. @@ -4229,8 +4227,16 @@ Returns an accessibility snapshot of the page optimized for AI consumption. * since: v1.59 - `track` <[string]> -When specified, enables incremental snapshots. Subsequent calls with the same track name will return -an incremental snapshot containing only changes since the last call. +When specified, enables incremental snapshots. Subsequent calls with the same track name will +track changes between calls. + +### option: Page.snapshotForAI.mode +* since: v1.59 +- `mode` <[string]> + +When set to `"incremental"` and [`option: Page.snapshotForAI.track`] is specified, returns an +incremental snapshot containing only changes since the last call with the same track name. +Defaults to `"full"`. ### option: Page.snapshotForAI.depth * since: v1.59 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 98132f06fd933..86882d9abc8c7 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -4525,6 +4525,13 @@ export interface Page { */ depth?: number; + /** + * When set to `"incremental"` and + * [`track`](https://playwright.dev/docs/api/class-page#page-snapshot-for-ai-option-track) is specified, returns an + * incremental snapshot containing only changes since the last call with the same track name. Defaults to `"full"`. + */ + mode?: string; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -4534,22 +4541,11 @@ export interface Page { timeout?: number; /** - * When specified, enables incremental snapshots. Subsequent calls with the same track name will return an incremental - * snapshot containing only changes since the last call. + * When specified, enables incremental snapshots. Subsequent calls with the same track name will track changes between + * calls. */ track?: string; - }): Promise<{ - /** - * Full accessibility snapshot of the page. - */ - full: string; - - /** - * Incremental snapshot containing only changes since the last tracked snapshot, when using the - * [`track`](https://playwright.dev/docs/api/class-page#page-snapshot-for-ai-option-track) option. - */ - incremental?: string; - }>; + }): Promise; /** * **NOTE** Use locator-based [locator.tap([options])](https://playwright.dev/docs/api/class-locator#locator-tap) instead. Read @@ -14706,12 +14702,7 @@ export interface Locator { * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. */ timeout?: number; - }): Promise<{ - /** - * Accessibility snapshot of the element matching this locator. - */ - full: string; - }>; + }): Promise; /** * Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 6e543098eef4a..d92813f36e5a9 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -378,8 +378,9 @@ export class Locator implements api.Locator { await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, omitReturnValue: true, ...options, timeout: this._frame._timeout(options) }); } - async snapshotForAI(options: TimeoutOptions & { depth?: number } = {}): Promise<{ full: string }> { - return await this._frame._page!._channel.snapshotForAI({ timeout: this._frame._timeout(options), selector: this._selector, depth: options.depth }); + async snapshotForAI(options: TimeoutOptions & { depth?: number } = {}): Promise { + const result = await this._frame._page!._channel.snapshotForAI({ timeout: this._frame._timeout(options), selector: this._selector, depth: options.depth }); + return result.snapshot; } async _expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean, errorMessage?: string }> { diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 3109aa8c5468e..0f9027608b426 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -851,8 +851,9 @@ export class Page extends ChannelOwner implements api.Page return result.pdf; } - async snapshotForAI(options: TimeoutOptions & { track?: string, depth?: number } = {}): Promise<{ full: string, incremental?: string }> { - return await this._channel.snapshotForAI({ timeout: this._timeoutSettings.timeout(options), track: options.track, depth: options.depth }); + async snapshotForAI(options: TimeoutOptions & { track?: string, mode?: 'full' | 'incremental', depth?: number } = {}): Promise { + const result = await this._channel.snapshotForAI({ timeout: this._timeoutSettings.timeout(options), track: options.track, mode: options.mode, depth: options.depth }); + return result.snapshot; } async _setDockTile(image: Buffer) { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 86a14ab62bd6f..3870bad4d7f8c 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1496,13 +1496,13 @@ scheme.PageRequestsResult = tObject({ }); scheme.PageSnapshotForAIParams = tObject({ track: tOptional(tString), + mode: tOptional(tEnum(['full', 'incremental'])), selector: tOptional(tString), depth: tOptional(tInt), timeout: tFloat, }); scheme.PageSnapshotForAIResult = tObject({ - full: tString, - incremental: tOptional(tString), + snapshot: tString, }); scheme.PageStartJSCoverageParams = tObject({ resetOnNavigation: tOptional(tBoolean), diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 567d2633baf86..ff45b15766cad 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -886,7 +886,7 @@ export class Page extends SdkObject { await Promise.all(this.frames().map(frame => frame.hideHighlight().catch(() => {}))); } - async snapshotForAI(progress: Progress, options: { track?: string, doNotRenderActive?: boolean, selector?: string, depth?: number } = {}): Promise<{ full: string, incremental?: string }> { + async snapshotForAI(progress: Progress, options: { track?: string, mode?: 'full' | 'incremental', doNotRenderActive?: boolean, selector?: string, depth?: number } = {}): Promise<{ snapshot: string }> { if (options.selector && options.track) throw new Error('Cannot specify both selector and track options'); @@ -902,8 +902,9 @@ export class Page extends SdkObject { frame = this.mainFrame(); } - const snapshot = await snapshotFrameForAI(progress, frame, { ...options, info }); - return { full: snapshot.full.join('\n'), incremental: snapshot.incremental?.join('\n') }; + const result = await snapshotFrameForAI(progress, frame, { ...options, info }); + const snapshot = options.mode === 'incremental' && result.incremental !== undefined ? result.incremental.join('\n') : result.full.join('\n'); + return { snapshot }; } async setDockTile(image: Buffer) { diff --git a/packages/playwright-core/src/tools/backend/response.ts b/packages/playwright-core/src/tools/backend/response.ts index 0e4941351faeb..2949a46c6f7b3 100644 --- a/packages/playwright-core/src/tools/backend/response.ts +++ b/packages/playwright-core/src/tools/backend/response.ts @@ -197,7 +197,7 @@ export class Response { addSection('Ran Playwright code', this._code, 'js'); // Render tab titles upon changes or when more than one tab. - const tabSnapshot = this._context.currentTab() ? await this._context.currentTabOrDie().captureSnapshot(this._includeSnapshotSelector, this._includeSnapshotDepth, this._clientWorkspace) : undefined; + const tabSnapshot = this._context.currentTab() ? await this._context.currentTabOrDie().captureSnapshot(this._includeSnapshotSelector, this._includeSnapshotDepth, this._clientWorkspace, this._includeSnapshot === 'incremental' ? 'incremental' : 'full') : undefined; const tabHeaders = await Promise.all(this._context.tabs().map(tab => tab.headerSnapshot())); if (this._includeSnapshot !== 'none' || tabHeaders.some(header => header.changed)) { if (tabHeaders.length !== 1) @@ -213,7 +213,7 @@ export class Response { // Handle tab snapshot if (tabSnapshot && this._includeSnapshot !== 'none') { - const snapshot = this._includeSnapshot === 'full' ? tabSnapshot.ariaSnapshot : tabSnapshot.ariaSnapshotDiff ?? tabSnapshot.ariaSnapshot; + const snapshot = tabSnapshot.ariaSnapshot; if (this._context.config.outputMode === 'file' || this._includeSnapshotFileName) { const resolvedFile = await this.resolveClientFile({ prefix: 'page', ext: 'yml', suggestedFilename: this._includeSnapshotFileName }, 'Snapshot'); await fs.promises.writeFile(resolvedFile.fileName, snapshot, 'utf-8'); diff --git a/packages/playwright-core/src/tools/backend/tab.ts b/packages/playwright-core/src/tools/backend/tab.ts index 4fb129b9ec979..c28edd06c8dff 100644 --- a/packages/playwright-core/src/tools/backend/tab.ts +++ b/packages/playwright-core/src/tools/backend/tab.ts @@ -84,7 +84,6 @@ export type TabHeader = { type TabSnapshot = { ariaSnapshot: string; - ariaSnapshotDiff?: string; modalStates: ModalState[]; events: EventEntry[]; consoleLink?: string; @@ -375,14 +374,15 @@ export class Tab extends EventEmitter { this._requests.length = 0; } - async captureSnapshot(selector: string | undefined, depth: number | undefined, relativeTo: string | undefined): Promise { + async captureSnapshot(selector: string | undefined, depth: number | undefined, relativeTo: string | undefined, mode: 'full' | 'incremental'): Promise { await this._initializedPromise; let tabSnapshot: TabSnapshot | undefined; const modalStates = await this._raceAgainstModalStates(async () => { - const snapshot: { full: string, incremental?: string } = selector ? await this.page.locator(selector).snapshotForAI({ depth }) : await this.page.snapshotForAI({ track: 'response', depth }); + const ariaSnapshot = selector + ? await this.page.locator(selector).snapshotForAI({ depth }) + : await this.page.snapshotForAI({ track: 'response', mode: this._needsFullSnapshot ? 'full' : mode, depth }); tabSnapshot = { - ariaSnapshot: snapshot.full, - ariaSnapshotDiff: this._needsFullSnapshot ? undefined : snapshot.incremental, + ariaSnapshot, modalStates: [], events: [], }; @@ -398,7 +398,6 @@ export class Tab extends EventEmitter { this._needsFullSnapshot = !tabSnapshot; return tabSnapshot ?? { ariaSnapshot: '', - ariaSnapshotDiff: '', modalStates, events: [], }; diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 98132f06fd933..86882d9abc8c7 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -4525,6 +4525,13 @@ export interface Page { */ depth?: number; + /** + * When set to `"incremental"` and + * [`track`](https://playwright.dev/docs/api/class-page#page-snapshot-for-ai-option-track) is specified, returns an + * incremental snapshot containing only changes since the last call with the same track name. Defaults to `"full"`. + */ + mode?: string; + /** * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` * option in the config, or by using the @@ -4534,22 +4541,11 @@ export interface Page { timeout?: number; /** - * When specified, enables incremental snapshots. Subsequent calls with the same track name will return an incremental - * snapshot containing only changes since the last call. + * When specified, enables incremental snapshots. Subsequent calls with the same track name will track changes between + * calls. */ track?: string; - }): Promise<{ - /** - * Full accessibility snapshot of the page. - */ - full: string; - - /** - * Incremental snapshot containing only changes since the last tracked snapshot, when using the - * [`track`](https://playwright.dev/docs/api/class-page#page-snapshot-for-ai-option-track) option. - */ - incremental?: string; - }>; + }): Promise; /** * **NOTE** Use locator-based [locator.tap([options])](https://playwright.dev/docs/api/class-locator#locator-tap) instead. Read @@ -14706,12 +14702,7 @@ export interface Locator { * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. */ timeout?: number; - }): Promise<{ - /** - * Accessibility snapshot of the element matching this locator. - */ - full: string; - }>; + }): Promise; /** * Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 3aa3db889a368..2ce170393a4b9 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -709,7 +709,7 @@ class ArtifactsRecorder { try { // TODO: maybe capture snapshot when the error is created, so it's from the right page and right time await page._wrapApiCall(async () => { - this._pageSnapshot = (await page.snapshotForAI({ timeout: 5000 })).full; + this._pageSnapshot = await page.snapshotForAI({ timeout: 5000 }); }, { internal: true }); } catch {} } diff --git a/packages/playwright/src/mcp/test/browserBackend.ts b/packages/playwright/src/mcp/test/browserBackend.ts index 0b3ac3718e31e..b6438a84a73c9 100644 --- a/packages/playwright/src/mcp/test/browserBackend.ts +++ b/packages/playwright/src/mcp/test/browserBackend.ts @@ -98,7 +98,7 @@ async function generatePausedMessage(testInfo: TestInfoImpl, context: playwright lines.push( `- Page Snapshot:`, '```yaml', - (await page.snapshotForAI()).full, + await page.snapshotForAI(), '```', ); } diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index d205475ae77d6..8512dcc0ddf77 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -2612,18 +2612,19 @@ export type PageRequestsResult = { }; export type PageSnapshotForAIParams = { track?: string, + mode?: 'full' | 'incremental', selector?: string, depth?: number, timeout: number, }; export type PageSnapshotForAIOptions = { track?: string, + mode?: 'full' | 'incremental', selector?: string, depth?: number, }; export type PageSnapshotForAIResult = { - full: string, - incremental?: string, + snapshot: string, }; export type PageStartJSCoverageParams = { resetOnNavigation?: boolean, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index c7f20e761a582..4c0ab686bce36 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2013,12 +2013,16 @@ Page: parameters: # When track is present, an incremental snapshot is returned when possible. track: string? + mode: + type: enum? + literals: + - full + - incremental selector: string? depth: int? timeout: float returns: - full: string - incremental: string? + snapshot: string startJSCoverage: title: Start JS coverage diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index d4cf11f00aa77..a4a0bc6753190 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -19,9 +19,8 @@ import { test as it, expect } from './pageTest'; import { unshift } from '../config/utils'; import type { Page } from 'playwright-core'; -async function snapshotForAI(page: Page, options?: Parameters[0] & { mode?: 'full' | 'incremental' }): Promise { - const snapshot = await page.snapshotForAI(options); - return options?.mode === 'incremental' ? snapshot.incremental : snapshot.full; +async function snapshotForAI(page: Page, options?: Parameters[0]): Promise { + return await page.snapshotForAI(options); } it('should generate refs', async ({ page }) => { @@ -717,7 +716,13 @@ it('should not create incremental snapshots without tracks', async ({ page }) => - button "a button" [ref=e4] - listitem [ref=e5]: a span `); - expect(await snapshotForAI(page, { mode: 'incremental' })).toBe(undefined); + // Without a track, mode: 'incremental' falls back to full snapshot. + expect(await snapshotForAI(page, { mode: 'incremental' })).toContainYaml(` + - list [ref=e2]: + - listitem [ref=e3]: + - button "a button" [ref=e4] + - listitem [ref=e5]: a span + `); }); it('should create incremental snapshot for children swap', async ({ page }) => { @@ -794,7 +799,7 @@ it('should limit depth', async ({ page }) => { - listitem [ref=e10]: item3 `); - const { full: snapshot4 } = await page.locator('#target').snapshotForAI({ depth: 1 }); + const snapshot4 = await page.locator('#target').snapshotForAI({ depth: 1 }); expect(snapshot4).toContainYaml(` - list [ref=e6]: - listitem [ref=e7]: item2