Skip to content

Commit 542d8a5

Browse files
committed
feat: support depth option in aria snapshot
1 parent 506b09b commit 542d8a5

11 files changed

Lines changed: 106 additions & 19 deletions

File tree

docs/src/api/class-page.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4233,6 +4233,12 @@ Returns an accessibility snapshot of the page optimized for AI consumption.
42334233
When specified, enables incremental snapshots. Subsequent calls with the same track name will return
42344234
an incremental snapshot containing only changes since the last call.
42354235

4236+
### option: Page.snapshotForAI.depth
4237+
* since: v1.59
4238+
- `depth` <[int]>
4239+
4240+
When specified, limits the depth of the snapshot.
4241+
42364242
## async method: Page.tap
42374243
* since: v1.8
42384244
* discouraged: Use locator-based [`method: Locator.tap`] instead. Read more about [locators](../locators.md).

packages/injected/src/ariaSnapshot.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export type AriaTreeOptions = {
4040
mode: 'ai' | 'expect' | 'codegen' | 'autoexpect';
4141
refPrefix?: string;
4242
doNotRenderActive?: boolean;
43+
depth?: number;
4344
};
4445

4546
type InternalOptions = {
@@ -563,6 +564,10 @@ function filterSnapshotDiff(nodes: (aria.AriaNode | string)[], statusMap: Map<ar
563564
return result;
564565
}
565566

567+
function indent(depth: number): string {
568+
return ' '.repeat(depth);
569+
}
570+
566571
export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTreeOptions, previousSnapshot?: AriaSnapshot): string {
567572
const options = toInternalOptions(publicOptions);
568573
const lines: string[] = [];
@@ -576,10 +581,12 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr
576581
if (previousSnapshot)
577582
nodesToRender = filterSnapshotDiff(nodesToRender, statusMap);
578583

579-
const visitText = (text: string, indent: string) => {
584+
const visitText = (text: string, depth: number) => {
585+
if (publicOptions.depth && depth > publicOptions.depth)
586+
return;
580587
const escaped = yamlEscapeValueIfNeeded(renderString(text));
581588
if (escaped)
582-
lines.push(indent + '- text: ' + escaped);
589+
lines.push(indent(depth) + '- text: ' + escaped);
583590
};
584591

585592
const createKey = (ariaNode: aria.AriaNode, renderCursorPointer: boolean): string => {
@@ -623,19 +630,24 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr
623630
return ariaNode?.children.length === 1 && typeof ariaNode.children[0] === 'string' && !Object.keys(ariaNode.props).length ? ariaNode.children[0] : undefined;
624631
};
625632

626-
const visit = (ariaNode: aria.AriaNode, indent: string, renderCursorPointer: boolean) => {
633+
const visit = (ariaNode: aria.AriaNode, depth: number, renderCursorPointer: boolean) => {
634+
if (publicOptions.depth && depth > publicOptions.depth)
635+
return;
636+
627637
// Replace the whole subtree with a single reference when possible.
628638
if (statusMap.get(ariaNode) === 'same' && ariaNode.ref) {
629-
lines.push(indent + `- ref=${ariaNode.ref} [unchanged]`);
639+
lines.push(indent(depth) + `- ref=${ariaNode.ref} [unchanged]`);
630640
return;
631641
}
632642

633643
// When producing a diff, add <changed> marker to all diff roots.
634-
const isDiffRoot = !!previousSnapshot && !indent;
635-
const escapedKey = indent + '- ' + (isDiffRoot ? '<changed> ' : '') + yamlEscapeKeyIfNeeded(createKey(ariaNode, renderCursorPointer));
644+
const isDiffRoot = !!previousSnapshot && !depth;
645+
const escapedKey = indent(depth) + '- ' + (isDiffRoot ? '<changed> ' : '') + yamlEscapeKeyIfNeeded(createKey(ariaNode, renderCursorPointer));
636646
const singleInlinedTextChild = getSingleInlinedTextChild(ariaNode);
647+
const isAtDepthLimit = !!publicOptions.depth && depth === publicOptions.depth;
648+
const hasNoChildren = !singleInlinedTextChild && (!ariaNode.children.length || isAtDepthLimit);
637649

638-
if (!ariaNode.children.length && !Object.keys(ariaNode.props).length) {
650+
if (hasNoChildren && !Object.keys(ariaNode.props).length) {
639651
// Leaf node without children.
640652
lines.push(escapedKey);
641653
} else if (singleInlinedTextChild !== undefined) {
@@ -649,24 +661,23 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr
649661
// Node with (optional) props and some children.
650662
lines.push(escapedKey + ':');
651663
for (const [name, value] of Object.entries(ariaNode.props))
652-
lines.push(indent + ' - /' + name + ': ' + yamlEscapeValueIfNeeded(value));
664+
lines.push(indent(depth + 1) + '- /' + name + ': ' + yamlEscapeValueIfNeeded(value));
653665

654-
const childIndent = indent + ' ';
655666
const inCursorPointer = !!ariaNode.ref && renderCursorPointer && aria.hasPointerCursor(ariaNode);
656667
for (const child of ariaNode.children) {
657668
if (typeof child === 'string')
658-
visitText(includeText(ariaNode, child) ? child : '', childIndent);
669+
visitText(includeText(ariaNode, child) ? child : '', depth + 1);
659670
else
660-
visit(child, childIndent, renderCursorPointer && !inCursorPointer);
671+
visit(child, depth + 1, renderCursorPointer && !inCursorPointer);
661672
}
662673
}
663674
};
664675

665676
for (const nodeToRender of nodesToRender) {
666677
if (typeof nodeToRender === 'string')
667-
visitText(nodeToRender, '');
678+
visitText(nodeToRender, 0);
668679
else
669-
visit(nodeToRender, '', !!options.renderCursorPointer);
680+
visit(nodeToRender, 0, !!options.renderCursorPointer);
670681
}
671682
return lines.join('\n');
672683
}

packages/injected/src/injectedScript.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ export class InjectedScript {
306306
return this.incrementalAriaSnapshot(node, options).full;
307307
}
308308

309-
incrementalAriaSnapshot(node: Node, options: AriaTreeOptions & { track?: string }): { full: string, incremental?: string, iframeRefs: string[] } {
309+
incrementalAriaSnapshot(node: Node, options: AriaTreeOptions & { track?: string, depth?: number }): { full: string, incremental?: string, iframeRefs: string[] } {
310310
if (node.nodeType !== Node.ELEMENT_NODE)
311311
throw this.createStacklessError('Can only capture aria snapshot of Element nodes.');
312312
const ariaSnapshot = generateAriaTree(node as Element, options);

packages/playwright-client/types/types.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4538,6 +4538,11 @@ export interface Page {
45384538
* @param options
45394539
*/
45404540
snapshotForAI(options?: {
4541+
/**
4542+
* When specified, limits the depth of the snapshot.
4543+
*/
4544+
depth?: number;
4545+
45414546
/**
45424547
* Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout`
45434548
* option in the config, or by using the

packages/playwright-core/src/client/page.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -855,8 +855,8 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
855855
return result.pdf;
856856
}
857857

858-
async snapshotForAI(options: TimeoutOptions & { track?: string } = {}): Promise<{ full: string, incremental?: string }> {
859-
return await this._channel.snapshotForAI({ timeout: this._timeoutSettings.timeout(options), track: options.track });
858+
async snapshotForAI(options: TimeoutOptions & { track?: string, depth?: number } = {}): Promise<{ full: string, incremental?: string }> {
859+
return await this._channel.snapshotForAI({ timeout: this._timeoutSettings.timeout(options), track: options.track, depth: options.depth });
860860
}
861861

862862
async _setDockTile(image: Buffer) {

packages/playwright-core/src/protocol/validator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,6 +1497,7 @@ scheme.PageRequestsResult = tObject({
14971497
scheme.PageSnapshotForAIParams = tObject({
14981498
track: tOptional(tString),
14991499
selector: tOptional(tString),
1500+
depth: tOptional(tInt),
15001501
timeout: tFloat,
15011502
});
15021503
scheme.PageSnapshotForAIResult = tObject({

packages/playwright-core/src/server/page.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -886,7 +886,7 @@ export class Page extends SdkObject<PageEventMap> {
886886
await Promise.all(this.frames().map(frame => frame.hideHighlight().catch(() => {})));
887887
}
888888

889-
async snapshotForAI(progress: Progress, options: { track?: string, doNotRenderActive?: boolean, selector?: string } = {}): Promise<{ full: string, incremental?: string }> {
889+
async snapshotForAI(progress: Progress, options: { track?: string, doNotRenderActive?: boolean, selector?: string, depth?: number } = {}): Promise<{ full: string, incremental?: string }> {
890890
if (options.selector && options.track)
891891
throw new Error('Cannot specify both selector and track options');
892892

@@ -1047,7 +1047,7 @@ export class InitScript extends DisposableObject {
10471047
}
10481048
}
10491049

1050-
async function snapshotFrameForAI(progress: Progress, frame: frames.Frame, options: { track?: string, doNotRenderActive?: boolean, info?: SelectorInfo } = {}): Promise<{ full: string[], incremental?: string[] }> {
1050+
async function snapshotFrameForAI(progress: Progress, frame: frames.Frame, options: { track?: string, doNotRenderActive?: boolean, info?: SelectorInfo, depth?: number } = {}): Promise<{ full: string[], incremental?: string[] }> {
10511051
// Only await the topmost navigations, inner frames will be empty when racing.
10521052
const snapshot = await frame.retryWithProgressAndTimeouts(progress, [1000, 2000, 4000, 8000], async continuePolling => {
10531053
try {
@@ -1069,6 +1069,7 @@ async function snapshotFrameForAI(progress: Progress, frame: frames.Frame, optio
10691069
track: options.track,
10701070
doNotRenderActive: options.doNotRenderActive,
10711071
info: options.info,
1072+
depth: options.depth,
10721073
}));
10731074
if (snapshotOrRetry === true)
10741075
return continuePolling;

packages/playwright-core/types/types.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4538,6 +4538,11 @@ export interface Page {
45384538
* @param options
45394539
*/
45404540
snapshotForAI(options?: {
4541+
/**
4542+
* When specified, limits the depth of the snapshot.
4543+
*/
4544+
depth?: number;
4545+
45414546
/**
45424547
* Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout`
45434548
* option in the config, or by using the

packages/protocol/src/channels.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2613,11 +2613,13 @@ export type PageRequestsResult = {
26132613
export type PageSnapshotForAIParams = {
26142614
track?: string,
26152615
selector?: string,
2616+
depth?: number,
26162617
timeout: number,
26172618
};
26182619
export type PageSnapshotForAIOptions = {
26192620
track?: string,
26202621
selector?: string,
2622+
depth?: number,
26212623
};
26222624
export type PageSnapshotForAIResult = {
26232625
full: string,

packages/protocol/src/protocol.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2014,6 +2014,7 @@ Page:
20142014
# When track is present, an incremental snapshot is returned when possible.
20152015
track: string?
20162016
selector: string?
2017+
depth: int?
20172018
timeout: float
20182019
returns:
20192020
full: string

0 commit comments

Comments
 (0)