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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ gh pr create --repo microsoft/playwright --head username:fix-39562 \
--title "fix(proxy): handle SOCKS proxy authentication" \
--body "$(cat <<'EOF'
## Summary
- <describe the change>
- <describe the change very! briefly>
Fixes https://github.com/microsoft/playwright/issues/39562
EOF
Expand Down
13 changes: 13 additions & 0 deletions docs/src/api/class-locator.md
Original file line number Diff line number Diff line change
Expand Up @@ -2485,6 +2485,19 @@ This method expects [Locator] to point to an
### option: Locator.setInputFiles.timeout = %%-input-timeout-js-%%
* since: v1.14

## async method: Locator.snapshotForAI
* since: v1.59
- returns: <[Object]>
- `full` <[string]> Accessibility snapshot of the element matching this locator.

Returns an accessibility snapshot of the element's subtree optimized for AI consumption.

### option: Locator.snapshotForAI.timeout = %%-input-timeout-%%
* since: v1.59

### option: Locator.snapshotForAI.timeout = %%-input-timeout-js-%%
* since: v1.59

## async method: Locator.tap
* since: v1.14

Expand Down
3 changes: 3 additions & 0 deletions docs/src/api/class-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -4223,6 +4223,9 @@ Returns an accessibility snapshot of the page optimized for AI consumption.
### option: Page.snapshotForAI.timeout = %%-input-timeout-%%
* since: v1.59

### option: Page.snapshotForAI.timeout = %%-input-timeout-js-%%
* since: v1.59

### option: Page.snapshotForAI.track
* since: v1.59
- `track` <[string]>
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@
"lint": "npm run eslint && npm run tsc && npm run doc && npm run check-deps && node utils/generate_channels.js && node utils/generate_types/ && npm run lint-tests && npm run test-types && npm run lint-packages",
"lint-packages": "node utils/workspace.js --ensure-consistent",
"lint-tests": "node utils/lint_tests.js",
"flint": "concurrently \"npm run eslint\" \"npm run tsc\" \"npm run doc\" \"npm run check-deps\" \"node utils/generate_channels.js\" \"node utils/generate_types/\" \"npm run lint-tests\" \"npm run test-types\" \"npm run lint-packages\" \"node utils/doclint/linting-code-snippets/cli.js --js-only\"",
"flint": "concurrently \"npm run eslint\" \"npm run tsc\" \"npm run doc\" \"npm run check-deps\" \"node utils/generate_channels.js\" \"npm run lint-tests\" \"npm run test-types\" \"npm run lint-packages\" \"node utils/doclint/linting-code-snippets/cli.js --js-only\"",
"clean": "node utils/build/clean.js",
"build": "node utils/build/build.js",
"watch": "node utils/build/build.js --watch --lint",
"test-types": "node utils/generate_types/ && tsc -p utils/generate_types/test/tsconfig.json && tsc -p ./tests/",
"test-types": "node utils/generate_types/ && concurrently \"tsc -p utils/generate_types/test/tsconfig.json\" \"tsc -p ./tests/\"",
"roll": "node utils/roll_browser.js",
"check-deps": "node utils/check_deps.js",
"build-android-driver": "./utils/build_android_driver.sh",
Expand Down
27 changes: 27 additions & 0 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4538,6 +4538,14 @@ export interface Page {
* @param options
*/
snapshotForAI(options?: {
/**
* 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
* [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout)
* or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.
*/
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.
Expand Down Expand Up @@ -14666,6 +14674,25 @@ export interface Locator {
timeout?: number;
}): Promise<void>;

/**
* Returns an accessibility snapshot of the element's subtree optimized for AI consumption.
* @param options
*/
snapshotForAI(options?: {
/**
* 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
* [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout)
* 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;
}>;

/**
* Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually
* dispatching touch events, see the [emulating legacy touch events](https://playwright.dev/docs/touch-events) page.
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/cli/DEPS.list
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
../utilsBundle.ts
../client/
../server/trace/viewer/
../tools/cli-client/program.ts
10 changes: 10 additions & 0 deletions packages/playwright-core/src/cli/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/tra
import { assert, getPackageManagerExecCommand } from '../utils';
import { wrapInASCIIBox } from '../server/utils/ascii';
import { dotenv, program } from '../utilsBundle';
import { program as cliProgram } from '../tools/cli-client/program';

import type { Browser } from '../client/browser';
import type { BrowserContext } from '../client/browserContext';
Expand Down Expand Up @@ -353,6 +354,15 @@ Examples:
$ show-trace
$ show-trace https://example.com/trace.zip`);

program
.command('cli', { hidden: true })
.allowExcessArguments(true)
.allowUnknownOption(true)
.action(async options => {
process.argv.splice(process.argv.indexOf('cli'), 1);
cliProgram();
});

type Options = {
browser: string;
channel?: string;
Expand Down
4 changes: 4 additions & 0 deletions packages/playwright-core/src/client/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,10 @@ 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 = {}): Promise<{ full: string }> {
return await this._frame._page!._channel.snapshotForAI({ timeout: this._frame._timeout(options), selector: this._selector });
}

async _expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean, errorMessage?: string }> {
return this._frame._expect(expression, {
...options,
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1493,6 +1493,7 @@ scheme.PageRequestsResult = tObject({
});
scheme.PageSnapshotForAIParams = tObject({
track: tOptional(tString),
selector: tOptional(tString),
timeout: tFloat,
});
scheme.PageSnapshotForAIResult = tObject({
Expand Down
42 changes: 36 additions & 6 deletions packages/playwright-core/src/server/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ import { LongStandingScope, assert, renderTitleForCall, trimStringWithEllipsis }
import { asLocator } from '../utils';
import { getComparator } from './utils/comparators';
import { debugLogger } from './utils/debugLogger';
import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser';
import { isInvalidSelectorError, stringifySelector } from '../utils/isomorphic/selectorParser';
import { ManualPromise } from '../utils/isomorphic/manualPromise';
import { parseEvaluationResultValue } from '../utils/isomorphic/utilityScriptSerializers';
import { compressCallLog } from './callLog';
import * as rawBindingsControllerSource from '../generated/bindingsControllerSource';
import { Screencast } from './screencast';
import { NonRecoverableDOMError } from './dom';

import type { Artifact } from './artifact';
import type { BrowserContextEventMap } from './browserContext';
Expand All @@ -48,6 +49,7 @@ import type * as types from './types';
import type { ImageComparatorOptions } from './utils/comparators';
import type * as channels from '@protocol/channels';
import type { BindingPayload } from '@injected/bindingsController';
import type { SelectorInfo } from './frameSelectors';

export interface PageDelegate {
readonly rawMouse: input.RawMouse;
Expand Down Expand Up @@ -884,8 +886,23 @@ 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 } = {}): Promise<{ full: string, incremental?: string }> {
const snapshot = await snapshotFrameForAI(progress, this.mainFrame(), options);
async snapshotForAI(progress: Progress, options: { track?: string, doNotRenderActive?: boolean, selector?: string } = {}): Promise<{ full: string, incremental?: string }> {
if (options.selector && options.track)
throw new Error('Cannot specify both selector and track options');

let frame: frames.Frame;
let info: SelectorInfo | undefined;
if (options.selector) {
const resolved = await this.mainFrame().selectors.resolveInjectedForSelector(options.selector, { strict: true });
if (!resolved)
throw new Error(`Selector "${options.selector}" did not resolve to any element`);
frame = resolved.frame;
info = resolved.info;
} else {
frame = this.mainFrame();
}

const snapshot = await snapshotFrameForAI(progress, frame, { ...options, info });
return { full: snapshot.full.join('\n'), incremental: snapshot.incremental?.join('\n') };
}

Expand Down Expand Up @@ -1030,20 +1047,33 @@ export class InitScript extends DisposableObject {
}
}

async function snapshotFrameForAI(progress: Progress, frame: frames.Frame, options: { track?: string, doNotRenderActive?: boolean } = {}): Promise<{ full: string[], incremental?: string[] }> {
async function snapshotFrameForAI(progress: Progress, frame: frames.Frame, options: { track?: string, doNotRenderActive?: boolean, info?: SelectorInfo } = {}): Promise<{ full: string[], incremental?: string[] }> {
// Only await the topmost navigations, inner frames will be empty when racing.
const snapshot = await frame.retryWithProgressAndTimeouts(progress, [1000, 2000, 4000, 8000], async continuePolling => {
try {
const context = await progress.race(frame._utilityContext());
const injectedScript = await progress.race(context.injectedScript());
const snapshotOrRetry = await progress.race(injectedScript.evaluate((injected, options) => {
if (options.info) {
const element = injected.querySelector(options.info.parsed, injected.document, options.info.strict);
if (!element)
return false;
return injected.incrementalAriaSnapshot(element, { mode: 'ai', ...options });
}
const node = injected.document.body;
if (!node)
return true;
return injected.incrementalAriaSnapshot(node, { mode: 'ai', ...options });
}, { refPrefix: frame.seq ? 'f' + frame.seq : '', track: options.track, doNotRenderActive: options.doNotRenderActive }));
}, {
refPrefix: frame.seq ? 'f' + frame.seq : '',
track: options.track,
doNotRenderActive: options.doNotRenderActive,
info: options.info,
}));
if (snapshotOrRetry === true)
return continuePolling;
if (snapshotOrRetry === false)
throw new NonRecoverableDOMError(`Selector "${stringifySelector(options.info!.parsed)}" does not match any element`);
return snapshotOrRetry;
} catch (e) {
if (frame.isNonRetriableError(e))
Expand Down Expand Up @@ -1093,7 +1123,7 @@ async function snapshotFrameRefForAI(progress: Progress, parentFrame: frames.Fra
if (!child)
return { full: [] };
try {
return await snapshotFrameForAI(progress, child.frame, options);
return await snapshotFrameForAI(progress, child.frame, { ...options, info: undefined });
} catch {
return { full: [] };
}
Expand Down
6 changes: 4 additions & 2 deletions packages/playwright-core/src/tools/backend/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class Response {
private _context: Context;
private _includeSnapshot: 'none' | 'full' | 'incremental' = 'none';
private _includeSnapshotFileName: string | undefined;
private _includeSnapshotSelector: string | undefined;
private _isClose: boolean = false;

readonly toolName: string;
Expand Down Expand Up @@ -126,9 +127,10 @@ export class Response {
this._includeSnapshot = this._context.config.snapshot?.mode || 'incremental';
}

setIncludeFullSnapshot(includeSnapshotFileName?: string) {
setIncludeFullSnapshot(includeSnapshotFileName?: string, selector?: string) {
this._includeSnapshot = 'full';
this._includeSnapshotFileName = includeSnapshotFileName;
this._includeSnapshotSelector = selector;
}

async serialize(): Promise<CallToolResult> {
Expand Down Expand Up @@ -193,7 +195,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._clientWorkspace) : undefined;
const tabSnapshot = this._context.currentTab() ? await this._context.currentTabOrDie().captureSnapshot(this._includeSnapshotSelector, this._clientWorkspace) : 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 Down
3 changes: 2 additions & 1 deletion packages/playwright-core/src/tools/backend/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ const snapshot = defineTool({
description: 'Capture accessibility snapshot of the current page, this is better than screenshot',
inputSchema: z.object({
filename: z.string().optional().describe('Save snapshot to markdown file instead of returning it in the response.'),
selector: z.string().optional().describe('Element selector of the root element to capture a partial snapshot instead of the whole page'),
}),
type: 'readOnly',
},

handle: async (context, params, response) => {
await context.ensureTab();
response.setIncludeFullSnapshot(params.filename);
response.setIncludeFullSnapshot(params.filename, params.selector);
},
});

Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/tools/backend/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,11 +372,11 @@ export class Tab extends EventEmitter<TabEventsInterface> {
this._requests.length = 0;
}

async captureSnapshot(relativeTo: string | undefined): Promise<TabSnapshot> {
async captureSnapshot(selector: string | undefined, relativeTo: string | undefined): Promise<TabSnapshot> {
await this._initializedPromise;
let tabSnapshot: TabSnapshot | undefined;
const modalStates = await this._raceAgainstModalStates(async () => {
const snapshot = await this.page.snapshotForAI({ track: 'response' });
const snapshot: { full: string, incremental?: string } = selector ? await this.page.locator(selector).snapshotForAI() : await this.page.snapshotForAI({ track: 'response' });
tabSnapshot = {
ariaSnapshot: snapshot.full,
ariaSnapshotDiff: this._needsFullSnapshot ? undefined : snapshot.incremental,
Expand Down
6 changes: 4 additions & 2 deletions packages/playwright-core/src/tools/cli-daemon/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,12 +328,14 @@ const snapshot = declareCommand({
name: 'snapshot',
description: 'Capture page snapshot to obtain element ref',
category: 'core',
args: z.object({}),
args: z.object({
element: z.string().optional().describe('Element selector of the root element to capture a partial snapshot instead of the whole page'),
}),
options: z.object({
filename: z.string().optional().describe('Save snapshot to markdown file instead of returning it in the response.'),
}),
toolName: 'browser_snapshot',
toolParams: ({ filename }) => ({ filename }),
toolParams: ({ filename, element }) => ({ filename, selector: element }),
});

const evaluate = declareCommand({
Expand Down
3 changes: 3 additions & 0 deletions packages/playwright-core/src/tools/mcp/DEPS.list
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@
[browserFactory.ts]
../../client/connect.ts
../utils/connect.ts

[program.ts]
../../cli/program.ts
4 changes: 2 additions & 2 deletions packages/playwright-core/src/tools/mcp/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ export function decorateMCPInstallBrowserCommand(command: Command) {
.option('--only-shell', 'only install headless shell when installing chromium')
.option('--no-shell', 'do not install chromium headless shell')
.action(async options => {
const { program } = require('../program');
const argv = process.argv.map(arg => arg === 'install-browser' ? 'install' : arg);
program.parse(argv);
const { program: mainProgram } = await import('../../cli/program');
mainProgram.parse(argv);
});
}
27 changes: 27 additions & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4538,6 +4538,14 @@ export interface Page {
* @param options
*/
snapshotForAI(options?: {
/**
* 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
* [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout)
* or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.
*/
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.
Expand Down Expand Up @@ -14666,6 +14674,25 @@ export interface Locator {
timeout?: number;
}): Promise<void>;

/**
* Returns an accessibility snapshot of the element's subtree optimized for AI consumption.
* @param options
*/
snapshotForAI(options?: {
/**
* 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
* [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout)
* 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;
}>;

/**
* Perform a tap gesture on the element matching the locator. For examples of emulating other gestures by manually
* dispatching touch events, see the [emulating legacy touch events](https://playwright.dev/docs/touch-events) page.
Expand Down
2 changes: 2 additions & 0 deletions packages/protocol/src/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2608,10 +2608,12 @@ export type PageRequestsResult = {
};
export type PageSnapshotForAIParams = {
track?: string,
selector?: string,
timeout: number,
};
export type PageSnapshotForAIOptions = {
track?: string,
selector?: string,
};
export type PageSnapshotForAIResult = {
full: string,
Expand Down
1 change: 1 addition & 0 deletions packages/protocol/src/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2012,6 +2012,7 @@ Page:
parameters:
# When track is present, an incremental snapshot is returned when possible.
track: string?
selector: string?
timeout: float
returns:
full: string
Expand Down
Loading
Loading