Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions docs/src/api/class-locator.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
16 changes: 11 additions & 5 deletions docs/src/api/class-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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]>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it an enum please!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You' make it enum as you add the literal :)


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
Expand Down
31 changes: 11 additions & 20 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string>;

/**
* **NOTE** Use locator-based [locator.tap([options])](https://playwright.dev/docs/api/class-locator#locator-tap) instead. Read
Expand Down Expand Up @@ -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<string>;

/**
* Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually
Expand Down
5 changes: 3 additions & 2 deletions packages/playwright-core/src/client/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
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 }> {
Expand Down
5 changes: 3 additions & 2 deletions packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -851,8 +851,9 @@ export class Page extends ChannelOwner<channels.PageChannel> 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<string> {
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) {
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
7 changes: 4 additions & 3 deletions packages/playwright-core/src/server/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ export class Page extends SdkObject<PageEventMap> {
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');

Expand All @@ -902,8 +902,9 @@ export class Page extends SdkObject<PageEventMap> {
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) {
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/tools/backend/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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');
Expand Down
11 changes: 5 additions & 6 deletions packages/playwright-core/src/tools/backend/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ export type TabHeader = {

type TabSnapshot = {
ariaSnapshot: string;
ariaSnapshotDiff?: string;
modalStates: ModalState[];
events: EventEntry[];
consoleLink?: string;
Expand Down Expand Up @@ -375,14 +374,15 @@ export class Tab extends EventEmitter<TabEventsInterface> {
this._requests.length = 0;
}

async captureSnapshot(selector: string | undefined, depth: number | undefined, relativeTo: string | undefined): Promise<TabSnapshot> {
async captureSnapshot(selector: string | undefined, depth: number | undefined, relativeTo: string | undefined, mode: 'full' | 'incremental'): Promise<TabSnapshot> {
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: [],
};
Expand All @@ -398,7 +398,6 @@ export class Tab extends EventEmitter<TabEventsInterface> {
this._needsFullSnapshot = !tabSnapshot;
return tabSnapshot ?? {
ariaSnapshot: '',
ariaSnapshotDiff: '',
modalStates,
events: [],
};
Expand Down
31 changes: 11 additions & 20 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string>;

/**
* **NOTE** Use locator-based [locator.tap([options])](https://playwright.dev/docs/api/class-locator#locator-tap) instead. Read
Expand Down Expand Up @@ -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<string>;

/**
* Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/src/mcp/test/browserBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ async function generatePausedMessage(testInfo: TestInfoImpl, context: playwright
lines.push(
`- Page Snapshot:`,
'```yaml',
(await page.snapshotForAI()).full,
await page.snapshotForAI(),
'```',
);
}
Expand Down
5 changes: 3 additions & 2 deletions packages/protocol/src/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 6 additions & 2 deletions packages/protocol/src/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions tests/page/page-aria-snapshot-ai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Page['snapshotForAI']>[0] & { mode?: 'full' | 'incremental' }): Promise<string> {
const snapshot = await page.snapshotForAI(options);
return options?.mode === 'incremental' ? snapshot.incremental : snapshot.full;
async function snapshotForAI(page: Page, options?: Parameters<Page['snapshotForAI']>[0]): Promise<string> {
return await page.snapshotForAI(options);
}

it('should generate refs', async ({ page }) => {
Expand Down Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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
Expand Down
Loading