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
12 changes: 8 additions & 4 deletions packages/injected/src/ariaSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,8 @@ export function matchesExpectAriaTemplate(rootElement: Element, template: aria.A
return {
matches,
received: {
raw: renderAriaTree(snapshot, { mode: 'default' }),
regex: renderAriaTree(snapshot, { mode: 'codegen' }),
raw: renderAriaTree(snapshot, { mode: 'default' }).text,
regex: renderAriaTree(snapshot, { mode: 'codegen' }).text,
}
};
}
Expand Down Expand Up @@ -568,9 +568,10 @@ function indent(depth: number): string {
return ' '.repeat(depth);
}

export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTreeOptions, previousSnapshot?: AriaSnapshot): string {
export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTreeOptions, previousSnapshot?: AriaSnapshot): { text: string, iframeDepths: Record<string, number> } {
const options = toInternalOptions(publicOptions);
const lines: string[] = [];
const iframeDepths: Record<string, number> = {};
const includeText = options.renderStringsAsRegex ? textContributesInfo : () => true;
const renderString = options.renderStringsAsRegex ? convertToBestGuessRegex : (str: string) => str;

Expand Down Expand Up @@ -634,6 +635,9 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr
if (publicOptions.depth && depth > publicOptions.depth)
return;

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]`);
Expand Down Expand Up @@ -679,7 +683,7 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr
else
visit(nodeToRender, 0, !!options.renderCursorPointer);
}
return lines.join('\n');
return { text: lines.join('\n'), iframeDepths };
}

function convertToBestGuessRegex(text: string): string {
Expand Down
10 changes: 5 additions & 5 deletions packages/injected/src/injectedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,25 +306,25 @@ export class InjectedScript {
return this.incrementalAriaSnapshot(node, options).full;
}

incrementalAriaSnapshot(node: Node, options: AriaTreeOptions & { track?: string, depth?: number }): { full: string, incremental?: string, iframeRefs: string[] } {
incrementalAriaSnapshot(node: Node, options: AriaTreeOptions & { track?: string, depth?: number }): { full: string, incremental?: string, iframeRefs: string[], iframeDepths: Record<string, number> } {
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 full = renderAriaTree(ariaSnapshot, 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);
incremental = renderAriaTree(ariaSnapshot, options, previousSnapshot).text;
this._lastAriaSnapshotForTrack.set(options.track, ariaSnapshot);
}
this._lastAriaSnapshotForQuery = ariaSnapshot;
return { full, incremental, iframeRefs: ariaSnapshot.iframeRefs };
return { full: rendered.text, incremental, iframeRefs: ariaSnapshot.iframeRefs, iframeDepths: rendered.iframeDepths };
}

ariaSnapshotForRecorder(): { ariaSnapshot: string, refs: Map<Element, string> } {
const tree = generateAriaTree(this.document.body, { mode: 'ai' });
const ariaSnapshot = renderAriaTree(tree, { mode: 'ai' });
const { text: ariaSnapshot } = renderAriaTree(tree, { mode: 'ai' });
return { ariaSnapshot, refs: tree.refs };
}

Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/client/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ export class Locator implements api.Locator {
}

async ariaSnapshot(options: TimeoutOptions & { mode?: 'ai' | 'default', depth?: number } = {}): Promise<string> {
const result = await this._frame._page!._channel.ariaSnapshot({ timeout: this._frame._timeout(options), mode: options.mode, selector: this._selector, depth: options.depth });
const result = await this._frame._channel.ariaSnapshot({ timeout: this._frame._timeout(options), mode: options.mode, selector: this._selector, depth: options.depth });
return result.snapshot;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}

async ariaSnapshot(options: TimeoutOptions & { mode?: 'ai' | 'default', depth?: number, _track?: string } = {}): Promise<string> {
const result = await this._channel.ariaSnapshot({ timeout: this._timeoutSettings.timeout(options), track: options._track, mode: options.mode, depth: options.depth });
const result = await this.mainFrame()._channel.ariaSnapshot({ timeout: this._timeoutSettings.timeout(options), track: options._track, mode: options.mode, depth: options.depth });
return result.snapshot;
}

Expand Down
20 changes: 10 additions & 10 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1494,16 +1494,6 @@ scheme.PageRequestsParams = tOptional(tObject({}));
scheme.PageRequestsResult = tObject({
requests: tArray(tChannel(['Request'])),
});
scheme.PageAriaSnapshotParams = tObject({
mode: tOptional(tEnum(['ai', 'default'])),
track: tOptional(tString),
selector: tOptional(tString),
depth: tOptional(tInt),
timeout: tFloat,
});
scheme.PageAriaSnapshotResult = tObject({
snapshot: tString,
});
scheme.PageStartJSCoverageParams = tObject({
resetOnNavigation: tOptional(tBoolean),
reportAnonymousScripts: tOptional(tBoolean),
Expand Down Expand Up @@ -1630,6 +1620,16 @@ scheme.FrameAddStyleTagParams = tObject({
scheme.FrameAddStyleTagResult = tObject({
element: tChannel(['ElementHandle']),
});
scheme.FrameAriaSnapshotParams = tObject({
mode: tOptional(tEnum(['ai', 'default'])),
track: tOptional(tString),
selector: tOptional(tString),
depth: tOptional(tInt),
timeout: tFloat,
});
scheme.FrameAriaSnapshotResult = tObject({
snapshot: tString,
});
scheme.FrameBlurParams = tObject({
selector: tString,
strict: tOptional(tBoolean),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Br
return { element: ElementHandleDispatcher.from(this, await progress.race(this._frame.addStyleTag(params))) };
}

async ariaSnapshot(params: channels.FrameAriaSnapshotParams, progress: Progress): Promise<channels.FrameAriaSnapshotResult> {
return await this._frame.ariaSnapshot(progress, params);
}

async click(params: channels.FrameClickParams, progress: Progress): Promise<void> {
progress.metadata.potentiallyClosesScope = true;
return await this._frame.click(progress, params.selector, params);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,10 +344,6 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
return { requests: this._page.networkRequests().map(request => RequestDispatcher.from(this.parentScope(), request)) };
}

async ariaSnapshot(params: channels.PageAriaSnapshotParams, progress: Progress): Promise<channels.PageAriaSnapshotResult> {
return await this._page.ariaSnapshot(progress, params);
}

async bringToFront(params: channels.PageBringToFrontParams, progress: Progress): Promise<void> {
await progress.race(this._page.bringToFront());
}
Expand Down
24 changes: 23 additions & 1 deletion packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { helper } from './helper';
import { SdkObject } from './instrumentation';
import * as js from './javascript';
import * as network from './network';
import { Page } from './page';
import { Page, ariaSnapshotForFrame } from './page';
import { isAbortError, ProgressController } from './progress';
import * as types from './types';
import { LongStandingScope, asLocator, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, renderTitleForCall } from '../utils';
Expand All @@ -36,6 +36,7 @@ import { ManualPromise } from '../utils/isomorphic/manualPromise';
import { compressCallLog } from './callLog';

import type { ConsoleMessage } from './console';
import type { SelectorInfo } from './frameSelectors';
import type { ElementStateWithoutStable, FrameExpectParams, InjectedScript } from '@injected/injectedScript';
import type { Progress } from './progress';
import type { ScreenshotOptions } from './screenshotter';
Expand Down Expand Up @@ -1690,6 +1691,27 @@ export class Frame extends SdkObject<FrameEventMap> {
}, { source, arg });
}

async ariaSnapshot(progress: Progress, options: { mode?: 'ai' | 'default', track?: string, doNotRenderActive?: boolean, selector?: string, depth?: number } = {}): Promise<{ snapshot: string }> {
if (options.selector && options.track)
throw new Error('Cannot specify both selector and track options');

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

private _asLocator(selector: string) {
return asLocator(this._page.browserContext._browser.sdkLanguage(), selector);
}
Expand Down
39 changes: 12 additions & 27 deletions packages/playwright-core/src/server/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -886,27 +886,6 @@ export class Page extends SdkObject<PageEventMap> {
await Promise.all(this.frames().map(frame => frame.hideHighlight().catch(() => {})));
}

async ariaSnapshot(progress: Progress, options: { mode?: 'ai' | 'default', track?: string, doNotRenderActive?: boolean, selector?: string, depth?: number } = {}): Promise<{ snapshot: 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 result = await ariaSnapshotForFrame(progress, frame, { ...options, info });
const snapshot = options.track && result.incremental ? result.incremental.join('\n') : result.full.join('\n');
return { snapshot };
}

async setDockTile(image: Buffer) {
await this.delegate.setDockTile(image);
}
Expand Down Expand Up @@ -1048,7 +1027,7 @@ export class InitScript extends DisposableObject {
}
}

async function ariaSnapshotForFrame(progress: Progress, frame: frames.Frame, options: { mode?: 'ai' | 'default', track?: string, doNotRenderActive?: boolean, info?: SelectorInfo, depth?: number } = {}): Promise<{ full: string[], incremental?: string[] }> {
export async function ariaSnapshotForFrame(progress: Progress, frame: frames.Frame, options: { mode?: 'ai' | 'default', track?: string, doNotRenderActive?: boolean, info?: SelectorInfo, depth?: number } = {}): 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 {
Expand Down Expand Up @@ -1085,20 +1064,26 @@ async function ariaSnapshotForFrame(progress: Progress, frame: frames.Frame, opt
}
});

const childSnapshotPromises = snapshot.iframeRefs.map(ref => ariaSnapshotFrameRef(progress, frame, ref, options));
// Only fetch child snapshots for iframes that were actually rendered (not filtered by depth).
const renderedIframeRefs = snapshot.iframeRefs.filter(ref => ref in snapshot.iframeDepths);
const childSnapshotPromises = renderedIframeRefs.map(ref => {
const iframeDepth = snapshot.iframeDepths[ref];
const childDepth = options.depth ? options.depth - iframeDepth - 1 : undefined;
return ariaSnapshotFrameRef(progress, frame, ref, { ...options, depth: childDepth });
});
const childSnapshots = await Promise.all(childSnapshotPromises);

const full = [];
let incremental: string[] | undefined;

if (snapshot.incremental !== undefined) {
incremental = snapshot.incremental.split('\n');
for (let i = 0; i < snapshot.iframeRefs.length; i++) {
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('- <changed> iframe [ref=' + snapshot.iframeRefs[i] + ']:', ...childSnapshot.full.map(l => ' ' + l));
incremental.push('- <changed> iframe [ref=' + renderedIframeRefs[i] + ']:', ...childSnapshot.full.map(l => ' ' + l));
}
}

Expand All @@ -1111,15 +1096,15 @@ async function ariaSnapshotForFrame(progress: Progress, frame: frames.Frame, opt

const leadingSpace = match[1];
const ref = match[2];
const childSnapshot = childSnapshots[snapshot.iframeRefs.indexOf(ref)] ?? { full: [] };
const childSnapshot = childSnapshots[renderedIframeRefs.indexOf(ref)] ?? { full: [] };
full.push(childSnapshot.full.length ? line + ':' : line);
full.push(...childSnapshot.full.map(l => leadingSpace + ' ' + l));
}

return { full, incremental };
}

async function ariaSnapshotFrameRef(progress: Progress, parentFrame: frames.Frame, frameRef: string, options: { mode?: 'ai' | 'default', track?: string, doNotRenderActive?: boolean }): Promise<{ full: string[], incremental?: string[] }> {
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[] }> {
const frameSelector = `aria-ref=${frameRef} >> internal:control=enter-frame`;
const frameBodySelector = `${frameSelector} >> body`;
const child = await progress.race(parentFrame.selectors.resolveFrameForSelector(frameBodySelector, { strict: true }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ export const methodMetainfo = new Map<string, { internal?: boolean, title?: stri
['Page.pageErrors', { title: 'Get page errors', group: 'getter', }],
['Page.pdf', { title: 'PDF', }],
['Page.requests', { title: 'Get network requests', group: 'getter', }],
['Page.ariaSnapshot', { title: 'Aria snapshot', group: 'getter', }],
['Page.startJSCoverage', { title: 'Start JS coverage', group: 'configuration', }],
['Page.stopJSCoverage', { title: 'Stop JS coverage', group: 'configuration', }],
['Page.startCSSCoverage', { title: 'Start CSS coverage', group: 'configuration', }],
Expand All @@ -157,6 +156,7 @@ export const methodMetainfo = new Map<string, { internal?: boolean, title?: stri
['Frame.evalOnSelectorAll', { title: 'Evaluate', snapshot: true, pausesBeforeAction: true, }],
['Frame.addScriptTag', { title: 'Add script tag', snapshot: true, pausesBeforeAction: true, }],
['Frame.addStyleTag', { title: 'Add style tag', snapshot: true, pausesBeforeAction: true, }],
['Frame.ariaSnapshot', { title: 'Aria snapshot', group: 'getter', }],
['Frame.blur', { title: 'Blur', slowMo: true, snapshot: true, pausesBeforeAction: true, }],
['Frame.check', { title: 'Check', slowMo: true, snapshot: true, pausesBeforeInput: true, }],
['Frame.click', { title: 'Click', slowMo: true, snapshot: true, pausesBeforeInput: true, }],
Expand Down
34 changes: 17 additions & 17 deletions packages/protocol/src/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2140,7 +2140,6 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
pageErrors(params: PagePageErrorsParams, progress?: Progress): Promise<PagePageErrorsResult>;
pdf(params: PagePdfParams, progress?: Progress): Promise<PagePdfResult>;
requests(params?: PageRequestsParams, progress?: Progress): Promise<PageRequestsResult>;
ariaSnapshot(params: PageAriaSnapshotParams, progress?: Progress): Promise<PageAriaSnapshotResult>;
startJSCoverage(params: PageStartJSCoverageParams, progress?: Progress): Promise<PageStartJSCoverageResult>;
stopJSCoverage(params?: PageStopJSCoverageParams, progress?: Progress): Promise<PageStopJSCoverageResult>;
startCSSCoverage(params: PageStartCSSCoverageParams, progress?: Progress): Promise<PageStartCSSCoverageResult>;
Expand Down Expand Up @@ -2610,22 +2609,6 @@ export type PageRequestsOptions = {};
export type PageRequestsResult = {
requests: RequestChannel[],
};
export type PageAriaSnapshotParams = {
mode?: 'ai' | 'default',
track?: string,
selector?: string,
depth?: number,
timeout: number,
};
export type PageAriaSnapshotOptions = {
mode?: 'ai' | 'default',
track?: string,
selector?: string,
depth?: number,
};
export type PageAriaSnapshotResult = {
snapshot: string,
};
export type PageStartJSCoverageParams = {
resetOnNavigation?: boolean,
reportAnonymousScripts?: boolean,
Expand Down Expand Up @@ -2767,6 +2750,7 @@ export interface FrameChannel extends FrameEventTarget, Channel {
evalOnSelectorAll(params: FrameEvalOnSelectorAllParams, progress?: Progress): Promise<FrameEvalOnSelectorAllResult>;
addScriptTag(params: FrameAddScriptTagParams, progress?: Progress): Promise<FrameAddScriptTagResult>;
addStyleTag(params: FrameAddStyleTagParams, progress?: Progress): Promise<FrameAddStyleTagResult>;
ariaSnapshot(params: FrameAriaSnapshotParams, progress?: Progress): Promise<FrameAriaSnapshotResult>;
blur(params: FrameBlurParams, progress?: Progress): Promise<FrameBlurResult>;
check(params: FrameCheckParams, progress?: Progress): Promise<FrameCheckResult>;
click(params: FrameClickParams, progress?: Progress): Promise<FrameClickResult>;
Expand Down Expand Up @@ -2872,6 +2856,22 @@ export type FrameAddStyleTagOptions = {
export type FrameAddStyleTagResult = {
element: ElementHandleChannel,
};
export type FrameAriaSnapshotParams = {
mode?: 'ai' | 'default',
track?: string,
selector?: string,
depth?: number,
timeout: number,
};
export type FrameAriaSnapshotOptions = {
mode?: 'ai' | 'default',
track?: string,
selector?: string,
depth?: number,
};
export type FrameAriaSnapshotResult = {
snapshot: string,
};
export type FrameBlurParams = {
selector: string,
strict?: boolean,
Expand Down
Loading
Loading