From 668054915ebc73caa6303681244b603fc4898ff8 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 2 Jul 2026 11:11:12 -0700 Subject: [PATCH 1/6] feat(cli): add init-skills command to install the Playwright CLI skill (#41580) --- .../src/tools/cli-client/skill/SKILL.md | 6 +++--- .../src/tools/cli-daemon/program.ts | 2 +- packages/playwright-core/src/tools/index.ts | 2 +- packages/playwright/src/program.ts | 15 +++++++++++++++ 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/playwright-core/src/tools/cli-client/skill/SKILL.md b/packages/playwright-core/src/tools/cli-client/skill/SKILL.md index 2fa5d0e96f9fe..20b118340825d 100644 --- a/packages/playwright-core/src/tools/cli-client/skill/SKILL.md +++ b/packages/playwright-core/src/tools/cli-client/skill/SKILL.md @@ -326,13 +326,13 @@ playwright-cli kill-all ## Installation -If global `playwright-cli` command is not available, try a local version via `npx playwright-cli`: +If global `playwright-cli` command is not available, try a local version via `npx playwright cli`: ```bash -npx --no-install playwright-cli --version +npx --no-install playwright --version ``` -When local version is available, use `npx playwright-cli` in all commands. Otherwise, install `playwright-cli` as a global command: +When local version is available, use `npx playwright cli` in all commands. Otherwise, install `playwright-cli` as a global command: ```bash npm install -g @playwright/cli@latest diff --git a/packages/playwright-core/src/tools/cli-daemon/program.ts b/packages/playwright-core/src/tools/cli-daemon/program.ts index fdb3e7a05a823..1ff7ac722de0b 100644 --- a/packages/playwright-core/src/tools/cli-daemon/program.ts +++ b/packages/playwright-core/src/tools/cli-daemon/program.ts @@ -83,7 +83,7 @@ function globalConfigFile(): string { return path.join(process.env['PWTEST_CLI_GLOBAL_CONFIG'] ?? os.homedir(), '.playwright', 'cli.config.json'); } -async function initWorkspace(initSkills: string | undefined) { +export async function initWorkspace(initSkills: string | undefined) { const cwd = process.cwd(); const playwrightDir = path.join(cwd, '.playwright'); await fs.promises.mkdir(playwrightDir, { recursive: true }); diff --git a/packages/playwright-core/src/tools/index.ts b/packages/playwright-core/src/tools/index.ts index 24459b9b0dd19..ec3e606812f67 100644 --- a/packages/playwright-core/src/tools/index.ts +++ b/packages/playwright-core/src/tools/index.ts @@ -31,7 +31,7 @@ export { extractTrace, DirTraceLoaderBackend } from './trace/traceParser'; export { decorateMCPCommand } from './mcp/program'; export { program as cliProgram } from './cli-client/program'; export { generateHelp, generateHelpJSON } from './cli-daemon/helpGenerator'; -export { decorateProgram as decorateCliDaemonProgram } from './cli-daemon/program'; +export { decorateProgram as decorateCliDaemonProgram, initWorkspace } from './cli-daemon/program'; export { openDashboardApp, openDashboardForContext } from './dashboard/dashboardApp'; export type { ContextConfig } from './backend/context'; diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 8449c12090916..ed480d49aeef0 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -185,6 +185,20 @@ function addInitAgentsCommand(program: Command) { }); } +function addInitSkillsCommand(program: Command) { + const command = program.command('init-skills'); + command.description('Initialize the workspace and install Playwright CLI skills'); + const option = command.createOption('--loop ', 'Agentic loop provider'); + option.choices(['claude', 'generic']); + option.default('generic'); + command.addOption(option); + command.action(async opts => { + // Claude Code only reads skills from `.claude/skills`, every other agent reads + // them from the universal `.agents/skills` folder. + await tools.initWorkspace(opts.loop === 'claude' ? 'claude' : 'agents'); + }); +} + const kTraceModes: TraceMode[] = ['on', 'off', 'on-first-retry', 'on-all-retries', 'retain-on-failure', 'retain-on-first-failure', 'retain-on-failure-and-retries']; // Note: update docs/src/test-cli-js.md when you update this, program is the source of truth. @@ -237,3 +251,4 @@ addClearCacheCommand(program); addTestMCPServerCommand(program); addTestServerCommand(program); addInitAgentsCommand(program); +addInitSkillsCommand(program); From 33433954c9a1308911c7cac74dc6bfa8bada4951 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 2 Jul 2026 11:48:30 -0700 Subject: [PATCH 2/6] chore(aria): remove unused incremental ariaSnapshot track option (#41581) --- packages/injected/src/ariaSnapshot.ts | 93 +------ packages/injected/src/injectedScript.ts | 14 +- packages/isomorphic/ariaSnapshot.ts | 16 -- .../playwright-core/src/client/channels.d.ts | 2 - packages/playwright-core/src/client/page.ts | 4 +- .../playwright-core/src/protocol/validator.ts | 1 - .../playwright-core/src/server/channels.d.ts | 2 - packages/playwright-core/src/server/frames.ts | 10 +- packages/playwright-core/src/server/page.ts | 39 +-- packages/protocol/spec/frame.yml | 2 - tests/page/page-aria-snapshot-ai.spec.ts | 232 +----------------- 11 files changed, 25 insertions(+), 390 deletions(-) diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index c72d3c64918f8..bddb959f6652b 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -500,86 +500,11 @@ function matchesNodeDeep(root: aria.AriaNode, template: aria.AriaTemplateNode, c return results; } -function buildByRefMap(root: aria.AriaNode | undefined, map: Map = new Map()): Map { - if (root?.ref) - map.set(root.ref, root); - for (const child of root?.children || []) { - if (typeof child !== 'string') - buildByRefMap(child, map); - } - return map; -} - -function compareSnapshots(ariaSnapshot: AriaSnapshot, previousSnapshot: AriaSnapshot | undefined): Map { - const previousByRef = buildByRefMap(previousSnapshot?.root); - const result = new Map(); - - // Returns whether ariaNode is the same as previousNode. - const visit = (ariaNode: aria.AriaNode, previousNode: aria.AriaNode | undefined): boolean => { - let same: boolean = ariaNode.children.length === previousNode?.children.length && aria.ariaNodesEqual(ariaNode, previousNode); - let canBeSkipped = same; - - for (let childIndex = 0 ; childIndex < ariaNode.children.length; childIndex++) { - const child = ariaNode.children[childIndex]; - const previousChild = previousNode?.children[childIndex]; - if (typeof child === 'string') { - same &&= child === previousChild; - canBeSkipped &&= child === previousChild; - } else { - let previous = typeof previousChild !== 'string' ? previousChild : undefined; - if (child.ref) - previous = previousByRef.get(child.ref); - const sameChild = visit(child, previous); - // New child, different order of children, or changed child with no ref - - // we have to include this node to list children in the right order. - if (!previous || (!sameChild && !child.ref) || (previous !== previousChild)) - canBeSkipped = false; - same &&= (sameChild && previous === previousChild); - } - } - - result.set(ariaNode, same ? 'same' : (canBeSkipped ? 'skip' : 'changed')); - return same; - }; - - visit(ariaSnapshot.root, previousByRef.get(previousSnapshot?.root?.ref)); - return result; -} - -// Chooses only the changed parts of the snapshot and returns them as new roots. -function filterSnapshotDiff(nodes: (aria.AriaNode | string)[], statusMap: Map): (aria.AriaNode | string)[] { - const result: (aria.AriaNode | string)[] = []; - - const visit = (ariaNode: aria.AriaNode) => { - const status = statusMap.get(ariaNode); - if (status === 'same') { - // No need to render unchanged root at all. - } else if (status === 'skip') { - // Only render changed children. - for (const child of ariaNode.children) { - if (typeof child !== 'string') - visit(child); - } - } else { - // Render this node's subtree. - result.push(ariaNode); - } - }; - - for (const node of nodes) { - if (typeof node === 'string') - result.push(node); - else - visit(node); - } - return result; -} - function indent(depth: number): string { return ' '.repeat(depth); } -export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTreeOptions, previousSnapshot?: AriaSnapshot): { text: string, iframeDepths: Record } { +export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTreeOptions): { text: string, iframeDepths: Record } { const options = toInternalOptions(publicOptions); const lines: string[] = []; const iframeDepths: Record = {}; @@ -587,11 +512,7 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr const renderString = options.renderStringsAsRegex ? convertToBestGuessRegex : (str: string) => str; // Do not render the root fragment, just its children. - let nodesToRender = ariaSnapshot.root.role === 'fragment' ? ariaSnapshot.root.children : [ariaSnapshot.root]; - - const statusMap = compareSnapshots(ariaSnapshot, previousSnapshot); - if (previousSnapshot) - nodesToRender = filterSnapshotDiff(nodesToRender, statusMap); + const nodesToRender = ariaSnapshot.root.role === 'fragment' ? ariaSnapshot.root.children : [ariaSnapshot.root]; const visitText = (text: string, depth: number) => { if (publicOptions.depth && depth > publicOptions.depth) @@ -660,15 +581,7 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr if (ariaNode.role === 'iframe' && ariaNode.ref) iframeDepths[ariaNode.ref] = depth; - // Replace the whole subtree with a single reference when possible. - if (statusMap.get(ariaNode) === 'same' && ariaNode.ref) { - lines.push(indent(depth) + `- ref=${ariaNode.ref} [unchanged]`); - return; - } - - // When producing a diff, add marker to all diff roots. - const isDiffRoot = !!previousSnapshot && !depth; - const escapedKey = indent(depth) + '- ' + (isDiffRoot ? ' ' : '') + yamlEscapeKeyIfNeeded(createKey(ariaNode, renderCursorPointer)); + const escapedKey = indent(depth) + '- ' + yamlEscapeKeyIfNeeded(createKey(ariaNode, renderCursorPointer)); const singleInlinedTextChild = getSingleInlinedTextChild(ariaNode); const isAtDepthLimit = !!publicOptions.depth && depth === publicOptions.depth; const hasNoChildren = !singleInlinedTextChild && (!ariaNode.children.length || isAtDepthLimit); diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index 17af70020bee0..2c816c12a1b7b 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -103,7 +103,6 @@ export class InjectedScript { readonly window: Window & typeof globalThis; readonly document: Document; readonly consoleApi: ConsoleAPI; - private _lastAriaSnapshotForTrack = new Map(); private _lastAriaSnapshotForQuery: AriaSnapshot | undefined; // Recorder must use any external dependencies through InjectedScript. @@ -313,23 +312,16 @@ export class InjectedScript { } ariaSnapshot(node: Node, options: AriaTreeOptions): string { - return this.incrementalAriaSnapshot(node, options).full; + return this.ariaSnapshotWithRefs(node, options).text; } - incrementalAriaSnapshot(node: Node, options: AriaTreeOptions & { track?: string, depth?: number }): { full: string, incremental?: string, iframeRefs: string[], iframeDepths: Record } { + ariaSnapshotWithRefs(node: Node, options: AriaTreeOptions & { depth?: number }): { text: string, iframeRefs: string[], iframeDepths: Record } { if (node.nodeType !== Node.ELEMENT_NODE) throw this.createStacklessError('Can only capture aria snapshot of Element nodes.'); const ariaSnapshot = generateAriaTree(node as Element, options); const rendered = renderAriaTree(ariaSnapshot, options); - let incremental: string | undefined; - if (options.track) { - const previousSnapshot = this._lastAriaSnapshotForTrack.get(options.track); - if (previousSnapshot) - incremental = renderAriaTree(ariaSnapshot, options, previousSnapshot).text; - this._lastAriaSnapshotForTrack.set(options.track, ariaSnapshot); - } this._lastAriaSnapshotForQuery = ariaSnapshot; - return { full: rendered.text, incremental, iframeRefs: ariaSnapshot.iframeRefs, iframeDepths: rendered.iframeDepths }; + return { text: rendered.text, iframeRefs: ariaSnapshot.iframeRefs, iframeDepths: rendered.iframeDepths }; } ariaSnapshotForRecorder(): { ariaSnapshot: string, refs: Map } { diff --git a/packages/isomorphic/ariaSnapshot.ts b/packages/isomorphic/ariaSnapshot.ts index 6a856f31f6124..e710975467b45 100644 --- a/packages/isomorphic/ariaSnapshot.ts +++ b/packages/isomorphic/ariaSnapshot.ts @@ -24,7 +24,6 @@ export type AriaRole = 'alert' | 'alertdialog' | 'application' | 'article' | 'ba 'spinbutton' | 'status' | 'strong' | 'subscript' | 'superscript' | 'switch' | 'tab' | 'table' | 'tablist' | 'tabpanel' | 'term' | 'textbox' | 'time' | 'timer' | 'toolbar' | 'tooltip' | 'tree' | 'treegrid' | 'treeitem'; -// Note: please keep in sync with ariaPropsEqual() below. export type AriaProps = { checked?: boolean | 'mixed'; disabled?: boolean; @@ -42,7 +41,6 @@ export type AriaBox = { cursor?: string; }; -// Note: please keep in sync with ariaNodesEqual() below. export type AriaNode = AriaProps & { role: AriaRole | 'fragment' | 'iframe'; name: string; @@ -53,24 +51,10 @@ export type AriaNode = AriaProps & { props: Record; }; -export function ariaNodesEqual(a: AriaNode, b: AriaNode): boolean { - if (a.role !== b.role || a.name !== b.name) - return false; - if (!ariaPropsEqual(a, b) || hasPointerCursor(a) !== hasPointerCursor(b)) - return false; - const aKeys = Object.keys(a.props); - const bKeys = Object.keys(b.props); - return aKeys.length === bKeys.length && aKeys.every(k => a.props[k] === b.props[k]); -} - export function hasPointerCursor(ariaNode: AriaNode): boolean { return ariaNode.box.cursor === 'pointer'; } -function ariaPropsEqual(a: AriaProps, b: AriaProps): boolean { - return a.active === b.active && a.checked === b.checked && a.disabled === b.disabled && a.expanded === b.expanded && a.invalid === b.invalid && a.selected === b.selected && a.level === b.level && a.pressed === b.pressed; -} - // We pass parsed template between worlds using JSON, make it easy. export type AriaRegex = { pattern: string }; diff --git a/packages/playwright-core/src/client/channels.d.ts b/packages/playwright-core/src/client/channels.d.ts index 68be295d3a52b..9a022dc8a31e2 100644 --- a/packages/playwright-core/src/client/channels.d.ts +++ b/packages/playwright-core/src/client/channels.d.ts @@ -2370,7 +2370,6 @@ export type FrameAddStyleTagResult = { }; export type FrameAriaSnapshotParams = { mode?: 'ai' | 'default', - track?: string, selector?: string, depth?: number, boxes?: boolean, @@ -2378,7 +2377,6 @@ export type FrameAriaSnapshotParams = { }; export type FrameAriaSnapshotOptions = { mode?: 'ai' | 'default', - track?: string, selector?: string, depth?: number, boxes?: boolean, diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index cbf6882e6402d..1ee49a01dc88f 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -887,8 +887,8 @@ export class Page extends ChannelOwner implements api.Page return result.pdf; } - async ariaSnapshot(options: TimeoutOptions & { mode?: 'ai' | 'default', depth?: number, boxes?: boolean, _track?: string } = {}): Promise { - const result = await this.mainFrame()._channel.ariaSnapshot({ timeout: this._timeoutSettings.timeout(options), track: options._track, mode: options.mode, depth: options.depth, boxes: options.boxes }, options.signal); + async ariaSnapshot(options: TimeoutOptions & { mode?: 'ai' | 'default', depth?: number, boxes?: boolean } = {}): Promise { + const result = await this.mainFrame()._channel.ariaSnapshot({ timeout: this._timeoutSettings.timeout(options), mode: options.mode, depth: options.depth, boxes: options.boxes }, options.signal); return result.snapshot; } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 7a9640c1df7c6..e080fc07a1278 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1291,7 +1291,6 @@ scheme.FrameAddStyleTagResult = tObject({ }); scheme.FrameAriaSnapshotParams = tObject({ mode: tOptional(tEnum(['ai', 'default'])), - track: tOptional(tString), selector: tOptional(tString), depth: tOptional(tInt), boxes: tOptional(tBoolean), diff --git a/packages/playwright-core/src/server/channels.d.ts b/packages/playwright-core/src/server/channels.d.ts index 975dd370607e0..9f84b5f545d53 100644 --- a/packages/playwright-core/src/server/channels.d.ts +++ b/packages/playwright-core/src/server/channels.d.ts @@ -2373,7 +2373,6 @@ export type FrameAddStyleTagResult = { }; export type FrameAriaSnapshotParams = { mode?: 'ai' | 'default', - track?: string, selector?: string, depth?: number, boxes?: boolean, @@ -2381,7 +2380,6 @@ export type FrameAriaSnapshotParams = { }; export type FrameAriaSnapshotOptions = { mode?: 'ai' | 'default', - track?: string, selector?: string, depth?: number, boxes?: boolean, diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 6a6acc697857a..1cb22c816183c 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1851,10 +1851,7 @@ export class Frame extends SdkObject { }, { source, arg }); } - async ariaSnapshot(progress: Progress, options: { mode?: 'ai' | 'default', track?: string, doNotRenderActive?: boolean, selector?: string, depth?: number, boxes?: boolean } = {}): Promise<{ snapshot: string }> { - if (options.selector && options.track) - throw new Error('Cannot specify both selector and track options'); - + async ariaSnapshot(progress: Progress, options: { mode?: 'ai' | 'default', doNotRenderActive?: boolean, selector?: string, depth?: number, boxes?: boolean } = {}): Promise<{ snapshot: string }> { if (options.selector && options.mode !== 'ai') { // Non-ai locator snapshot is auto-waiting and does not include iframes. const snapshot = await this._retryWithProgressIfNotConnected(progress, options.selector, { strict: true, performActionPreChecks: true }, async (progress, handle) => { @@ -1875,9 +1872,8 @@ export class Frame extends SdkObject { targetFrame = this; } - const result = await ariaSnapshotForFrame(progress, targetFrame, { ...options, info }); - const snapshot = options.track && result.incremental ? result.incremental.join('\n') : result.full.join('\n'); - return { snapshot }; + const lines = await ariaSnapshotForFrame(progress, targetFrame, { ...options, info }); + return { snapshot: lines.join('\n') }; } private _asLocator(selector: string) { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index b637071310b03..14c194b8bdb38 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -1107,7 +1107,7 @@ export class InitScript extends DisposableObject { } } -export async function ariaSnapshotForFrame(progress: Progress, frame: frames.Frame, options: { mode?: 'ai' | 'default', track?: string, doNotRenderActive?: boolean, info?: SelectorInfo, depth?: number, boxes?: boolean } = {}): Promise<{ full: string[], incremental?: string[] }> { +export async function ariaSnapshotForFrame(progress: Progress, frame: frames.Frame, options: { mode?: 'ai' | 'default', doNotRenderActive?: boolean, info?: SelectorInfo, depth?: number, boxes?: boolean } = {}): Promise { // Only await the topmost navigations, inner frames will be empty when racing. const snapshot = await frame.retryWithProgressAndTimeouts(progress, [1000, 2000, 4000, 8000], async (progress, continuePolling) => { try { @@ -1118,16 +1118,15 @@ export async function ariaSnapshotForFrame(progress: Progress, frame: frames.Fra const element = injected.querySelector(options.info.parsed, injected.document, options.info.strict); if (!element) return false; - return injected.incrementalAriaSnapshot(element, options); + return injected.ariaSnapshotWithRefs(element, options); } const node = injected.document.body; if (!node) return true; - return injected.incrementalAriaSnapshot(node, options); + return injected.ariaSnapshotWithRefs(node, options); }, { mode: options.mode ?? 'default', refPrefix: frame.seq ? 'f' + frame.seq : '', - track: options.track, doNotRenderActive: options.doNotRenderActive, info: options.info, depth: options.depth, @@ -1156,47 +1155,35 @@ export async function ariaSnapshotForFrame(progress: Progress, frame: frames.Fra const childSnapshots = await Promise.all(childSnapshotPromises); progress.setAllowConcurrentOrNestedRaces(false); - const full = []; - let incremental: string[] | undefined; - - if (snapshot.incremental !== undefined) { - incremental = snapshot.incremental.split('\n'); - for (let i = 0; i < renderedIframeRefs.length; i++) { - const childSnapshot = childSnapshots[i]; - if (childSnapshot.incremental) - incremental.push(...childSnapshot.incremental); - else if (childSnapshot.full.length) - incremental.push('- iframe [ref=' + renderedIframeRefs[i] + ']:', ...childSnapshot.full.map(l => ' ' + l)); - } - } + const lines = []; - for (const line of snapshot.full.split('\n')) { + for (const line of snapshot.text.split('\n')) { const match = line.match(/^(\s*)- iframe (?:\[active\] )?\[ref=([^\]]*)\]/); if (!match) { - full.push(line); + lines.push(line); continue; } const leadingSpace = match[1]; const ref = match[2]; - const childSnapshot = childSnapshots[renderedIframeRefs.indexOf(ref)] ?? { full: [] }; - full.push(childSnapshot.full.length ? line + ':' : line); - full.push(...childSnapshot.full.map(l => leadingSpace + ' ' + l)); + const childSnapshot = childSnapshots[renderedIframeRefs.indexOf(ref)] ?? []; + lines.push(childSnapshot.length ? line + ':' : line); + lines.push(...childSnapshot.map(l => leadingSpace + ' ' + l)); } - return { full, incremental }; + return lines; } -async function ariaSnapshotFrameRef(progress: Progress, parentFrame: frames.Frame, frameRef: string, options: { mode?: 'ai' | 'default', track?: string, doNotRenderActive?: boolean, depth?: number }): Promise<{ full: string[], incremental?: string[] }> { +async function ariaSnapshotFrameRef(progress: Progress, parentFrame: frames.Frame, frameRef: string, options: { mode?: 'ai' | 'default', doNotRenderActive?: boolean, depth?: number }): Promise { const frameSelector = `aria-ref=${frameRef} >> internal:control=enter-frame`; const frameBodySelector = `${frameSelector} >> body`; const child = await progress.race(parentFrame.selectors.resolveFrameForSelector(frameBodySelector, { strict: true })); if (!child) - return { full: [] }; + return []; try { return await ariaSnapshotForFrame(progress, child.frame, { ...options, info: undefined }); } catch { - return { full: [] }; + return []; } } diff --git a/packages/protocol/spec/frame.yml b/packages/protocol/spec/frame.yml index ec1813aa96e79..849ba14302562 100644 --- a/packages/protocol/spec/frame.yml +++ b/packages/protocol/spec/frame.yml @@ -84,8 +84,6 @@ Frame: literals: - ai - default - # When track is present, an incremental snapshot is returned when possible. - track: string? selector: string? depth: int? boxes: boolean? diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index e77b8786ea1a7..c0c562ee46d82 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -19,7 +19,7 @@ import { test as it, expect } from './pageTest'; import { unshift } from '../config/utils'; import type { Page } from 'playwright-core'; -async function snapshotForAI(page: Page, options?: Omit[0], 'mode'> & { _track?: string }): Promise { +async function snapshotForAI(page: Page, options?: Omit[0], 'mode'>): Promise { return await page.ariaSnapshot({ ...options, mode: 'ai' }); } @@ -560,236 +560,6 @@ it('should not remove generic nodes with title', async ({ page }) => { `); }); -it('should create incremental snapshots on multiple tracks', async ({ page }) => { - await page.setContent(`
  • a span
`); - - expect(await snapshotForAI(page, { _track: 'first' })).toContainYaml(` - - list [ref=e2]: - - listitem [ref=e3]: - - button "a button" [ref=e4] - - listitem [ref=e5]: a span - `); - expect(await snapshotForAI(page, { _track: 'second' })).toContainYaml(` - - list [ref=e2]: - - listitem [ref=e3]: - - button "a button" [ref=e4] - - listitem [ref=e5]: a span - `); - expect(await snapshotForAI(page, { _track: 'first' })).toContainYaml(` - `); - - await page.evaluate(() => { - document.querySelector('span').textContent = 'changed span'; - document.getElementById('hidden-li').style.display = 'inline'; - }); - expect(await snapshotForAI(page, { _track: 'first' })).toContainYaml(` - - list [ref=e2]: - - ref=e3 [unchanged] - - listitem [ref=e5]: changed span - - listitem [ref=e6]: some text - `); - - await page.evaluate(() => { - document.querySelector('span').textContent = 'a span'; - document.getElementById('hidden-li').style.display = 'none'; - }); - expect(await snapshotForAI(page, { _track: 'first' })).toContainYaml(` - - list [ref=e2]: - - ref=e3 [unchanged] - - listitem [ref=e5]: a span - `); - expect(await snapshotForAI(page, { _track: 'second' })).toContainYaml(` - `); -}); - -it('should create incremental snapshot for attribute change', async ({ page }) => { - await page.setContent(``); - await page.evaluate(() => document.querySelector('button').focus()); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - button "a button" [active] [ref=e2] - `); - - await page.evaluate(() => document.querySelector('button').blur()); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - button "a button" [ref=e2] - `); -}); - -it('should create incremental snapshot for child removal', async ({ page }) => { - await page.setContent(`
  • some text
  • `); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - listitem [ref=e2]: - - button "a button" [ref=e3] - - text: some text - `); - - await page.evaluate(() => document.querySelector('span').remove()); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - listitem [ref=e2]: - - ref=e3 [unchanged] - `); -}); - -it('should create incremental snapshot for child addition', async ({ page }) => { - await page.setContent(`
  • some text
  • `); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - listitem [ref=e2]: - - button "a button" [ref=e3] - `); - - await page.evaluate(() => document.querySelector('span').style.display = 'inline'); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - listitem [ref=e2]: - - ref=e3 [unchanged] - - text: some text - `); -}); - -it('should create incremental snapshot for prop change', async ({ page }) => { - await page.setContent(`a link`); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - link "a link" [ref=e2] [cursor=pointer]: - - /url: about:blank - `); - - await page.evaluate(() => document.querySelector('a').setAttribute('href', 'https://playwright.dev')); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - link "a link" [ref=e2] [cursor=pointer]: - - /url: https://playwright.dev - `); -}); - -it('should create incremental snapshot for cursor change', async ({ page }) => { - await page.setContent(`a link`); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - link "a link" [ref=e2] [cursor=pointer]: - - /url: about:blank - `); - - await page.evaluate(() => document.querySelector('a').style.cursor = 'default'); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - link "a link" [ref=e2]: - - /url: about:blank - `); -}); - -it('should create incremental snapshot for name change', async ({ page }) => { - await page.setContent(``); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - button "a button" [ref=e2] - `); - - await page.evaluate(() => document.querySelector('span').textContent = 'new button'); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - button "new button" [ref=e3] - `); -}); - -it('should create incremental snapshot for text change', async ({ page }) => { - await page.setContent(`
  • an item
  • `); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - listitem [ref=e2]: an item - `); - - await page.evaluate(() => document.querySelector('span').textContent = 'new text'); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - listitem [ref=e2]: new text - `); -}); - -it('should produce incremental snapshot for iframes', async ({ page }) => { - await page.setContent(` - - `); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - iframe [ref=e2]: - - listitem [ref=f1e2]: - - button "a button" [ref=f1e3] - `); - - await page.frames()[1].evaluate(() => { - document.querySelector('span').style.display = 'block'; - document.querySelector('iframe').style.display = 'block'; - }); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - listitem [ref=f1e2]: - - generic [ref=f1e4]: outer text - - ref=f1e3 [unchanged] - - iframe [ref=f1e5] - - iframe [ref=f1e5]: - - listitem [ref=f2e2]: inner text - `); -}); - -it('should create multiple chunks in incremental snapshot', async ({ page }) => { - await page.setContent(` -
      -
    • item1
    • -
    • item2
    • -
    • item3
    • -
        -
      • to be removed
      • -
      • one more
      • -
      -
    - `); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - list [ref=e2]: - - listitem [ref=e3]: item1 - - listitem [ref=e4]: item2 - - listitem [ref=e5]: - - group [ref=e6]: item3 - - list [ref=e7]: - - listitem [ref=e8]: to be removed - - listitem [ref=e9]: one more - `); - - await page.evaluate(() => { - const spans = document.querySelectorAll('span'); - spans[0].textContent = 'new item1'; - spans[2].textContent = 'new item3'; - const button = document.createElement('button'); - button.textContent = 'button'; - spans[2].parentElement.appendChild(button); - document.querySelector('#to-remove').remove(); - }); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - listitem [ref=e3]: new item1 - - group [ref=e6]: - - text: new item3 - - button "button" [ref=e10] - - list [ref=e7]: - - ref=e9 [unchanged] - `); -}); - -it('should create incremental snapshot for children swap', async ({ page }) => { - await page.setContent(` -
      -
    • item 1
    • -
    • item 2
    • -
    - `); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - list [ref=e2]: - - listitem [ref=e3]: item 1 - - listitem [ref=e4]: item 2 - `); - - await page.evaluate(() => document.querySelector('ul').appendChild(document.querySelector('li'))); - expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - - list [ref=e2]: - - ref=e4 [unchanged] - - ref=e3 [unchanged] - `); -}); - it('should limit depth', async ({ page }) => { await page.setContent(`
      From bd390faeaa7a7e1b5b0328c03ce3960714624f90 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 2 Jul 2026 12:11:04 -0700 Subject: [PATCH 3/6] test(webkit): unskip right-click tests fixed by browser roll (#41602) --- tests/page/page-click.spec.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/page/page-click.spec.ts b/tests/page/page-click.spec.ts index bfe0691e5cf4d..8649e738ac1e8 100644 --- a/tests/page/page-click.spec.ts +++ b/tests/page/page-click.spec.ts @@ -1215,18 +1215,13 @@ it('should fire contextmenu event on right click in correct order', async ({ pag const entries = []; page.on('console', message => entries.push(message.text())); await page.getByRole('button', { name: 'Click me' }).click({ button: 'right' }); - if (browserName === 'webkit') - await expect.poll(() => entries).toEqual(['mousedown', 'contextmenu']); - else if (browserName === 'chromium' && isWindows) + if (browserName === 'chromium' && isWindows) await expect.poll(() => entries).toEqual(['mousedown', 'mouseup', 'contextmenu']); else await expect.poll(() => entries).toEqual(['mousedown', 'contextmenu', 'mouseup']); }); -it('should click after a right click', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/39246' } }, async ({ page, browserName }) => { - // On webkit the native context menu opened by the right click swallows the - // following left click, so the button never receives it. - it.fixme(browserName === 'webkit'); +it('should click after a right click', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/39246' } }, async ({ page }) => { await page.setContent(`