From d70649fbd169d8d55f5a60b87596c8f6379199d5 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 3 Aug 2026 13:58:08 -0700 Subject: [PATCH] feat(mcp): emit structured snapshot in --json responses Add Page.ariaSnapshotJSON and Locator.ariaSnapshotJSON that return the aria snapshot as a free form JSON object. MCP response sections can now carry structured content, so inline snapshots in --json replies are emitted as the node tree instead of YAML text. Fixes: https://github.com/microsoft/playwright/issues/42076 --- docs/src/api/class-locator.md | 55 ++++++ docs/src/api/class-page.md | 37 ++++ packages/injected/src/ariaSnapshot.ts | 68 +++++++ packages/injected/src/injectedScript.ts | 14 +- packages/isomorphic/ariaSnapshot.ts | 9 + packages/isomorphic/protocolMetainfo.ts | 1 + packages/playwright-client/types/types.d.ts | 116 ++++++++++++ .../playwright-core/src/client/channels.d.ts | 16 ++ .../playwright-core/src/client/locator.ts | 5 + packages/playwright-core/src/client/page.ts | 5 + .../playwright-core/src/server/channels.d.ts | 16 ++ .../src/server/dispatchers/frameDispatcher.ts | 4 + packages/playwright-core/src/server/frames.ts | 14 +- packages/playwright-core/src/server/page.ts | 60 ++++++ .../src/tools/backend/response.ts | 30 ++- .../playwright-core/src/tools/backend/tab.ts | 33 +++- packages/playwright-core/types/types.d.ts | 116 ++++++++++++ packages/protocol/spec/frame.yml | 15 ++ packages/protocol/src/validator.ts | 9 + tests/mcp/cli-json.spec.ts | 34 +++- tests/page/page-aria-snapshot-json.spec.ts | 173 ++++++++++++++++++ 21 files changed, 805 insertions(+), 25 deletions(-) create mode 100644 tests/page/page-aria-snapshot-json.spec.ts diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index c252b318fdd91..7a6d5957ce2de 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -239,6 +239,61 @@ When `true`, appends each element's bounding box as `[box=x,y,width,height]` to relative to the viewport, in CSS pixels, as returned by [`Element.getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). Defaults to `false`. +## async method: Locator.ariaSnapshotJSON +* since: v1.63 +* langs: js +- returns: <[Serializable]> + +Captures the aria snapshot of the given element as a free form JSON object. + +**Usage** + +```js +await page.getByRole('list').ariaSnapshotJSON(); +``` + +**Details** + +This method returns the same tree as [`method: Locator.ariaSnapshot`], serialized as a JSON value instead of YAML markup. +The result is a list of nodes, each node being either a plain string with static text, or an object with the following properties: +* `role` <[string]> Aria role of the element. +* `name` <[string]> Accessible name of the element, if any. +* `text` <[string]> Text content of the element, when it is the only child. +* `children` <[Array]> Child nodes and text fragments. +* Boolean and value properties for element state flags: `checked`, `disabled`, `expanded`, `active`, `invalid`, `level`, `pressed` and `selected`. +* Additional element properties, for example `url` for links and `placeholder` for text boxes. +* `ref` <[string]> Element reference for AI-optimized snapshots. +* `cursor` <[string]> Set to `"pointer"` for clickable elements in AI-optimized snapshots. +* `box` <[Object]> Bounding box of the element when [`option: Locator.ariaSnapshotJSON.boxes`] is set. + +### option: Locator.ariaSnapshotJSON.mode +* since: v1.63 +- `mode` <[AriaSnapshotMode]<"ai"|"default">> + +When set to `"ai"`, returns a snapshot optimized for AI consumption. Defaults to `"default"`. See details in [`method: Locator.ariaSnapshot`]. + +### option: Locator.ariaSnapshotJSON.timeout = %%-input-timeout-%% +* since: v1.63 + +### option: Locator.ariaSnapshotJSON.timeout = %%-input-timeout-js-%% +* since: v1.63 + +### option: Locator.ariaSnapshotJSON.signal = %%-input-signal-%% + +### option: Locator.ariaSnapshotJSON.depth +* since: v1.63 +- `depth` <[int]> + +When specified, limits the depth of the snapshot. + +### option: Locator.ariaSnapshotJSON.boxes +* since: v1.63 +- `boxes` <[boolean]> + +When `true`, includes each element's bounding box as a `box` property with `x`, `y`, `width` and `height`. Coordinates are +relative to the viewport, in CSS pixels, as returned by [`Element.getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). +Defaults to `false`. + ## async method: Locator.blur * since: v1.28 diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 423fc97eefcef..9cb83bef41cdf 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -4427,6 +4427,43 @@ When `true`, appends each element's bounding box as `[box=x,y,width,height]` to relative to the viewport, in CSS pixels, as returned by [`Element.getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). Defaults to `false`. +## async method: Page.ariaSnapshotJSON +* since: v1.63 +* langs: js +- returns: <[Serializable]> + +Captures the aria snapshot of the page as a free form JSON object. +Returns the same tree as [`method: Page.ariaSnapshot`], serialized as a JSON value instead of YAML markup. +See [`method: Locator.ariaSnapshotJSON`] for the details of the format. + +### option: Page.ariaSnapshotJSON.mode +* since: v1.63 +- `mode` <[AriaSnapshotMode]<"ai"|"default">> + +When set to `"ai"`, returns a snapshot optimized for AI consumption: including element references like `[ref=e2]` and snapshots of ` + `); + const json = await page.ariaSnapshotJSON({ mode: 'ai' }) as NodeJSON[]; + const iframe = findNode(json, node => node.role === 'iframe'); + expect(iframe?.ref).toBeTruthy(); + const button = findNode(iframe!.children!, node => node.role === 'button'); + expect(button?.name).toBe('In frame'); + expect(button?.ref).toMatch(/^f\d+e\d+$/); +}); + +it('should limit depth', async ({ page }) => { + await page.setContent(``); + expect(await page.ariaSnapshotJSON({ depth: 1 })).toEqual([ + { + role: 'list', + children: [ + { role: 'listitem' }, + ], + }, + ]); +}); + +it('should include boxes when requested', async ({ page }) => { + await page.setContent(``); + const json = await page.ariaSnapshotJSON({ boxes: true }) as NodeJSON[]; + const button = findNode(json, node => node.role === 'button'); + expect(button?.box).toEqual({ + x: expect.any(Number), + y: expect.any(Number), + width: expect.any(Number), + height: expect.any(Number), + }); + expect(button!.box.width).toBeGreaterThan(0); + expect(button!.box.height).toBeGreaterThan(0); +}); + +it('should snapshot a locator', async ({ page }) => { + await page.setContent(` +

title

+ + `); + expect(await page.locator('ul').ariaSnapshotJSON()).toEqual([ + { + role: 'list', + children: [ + { role: 'listitem', text: 'one' }, + { role: 'listitem', text: 'two' }, + ], + }, + ]); +});