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 packages/injected/src/ariaSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ function computeAriaRef(ariaNode: aria.AriaNode, options: InternalOptions) {

function toAriaNode(element: Element, options: InternalOptions, nameSourceElements: Map<aria.AriaNode, Set<Element> | undefined>): aria.AriaNode | null {
const active = element.ownerDocument.activeElement === element && element.ownerDocument.hasFocus();
if (element.nodeName === 'IFRAME') {
if (element.nodeName === 'IFRAME' || element.nodeName === 'FRAME') {
const ariaNode: aria.AriaNode = {
role: 'iframe',
name: '',
Expand Down
2 changes: 1 addition & 1 deletion packages/injected/src/roleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
}

// step 2i.
if (!['presentation', 'none'].includes(role) || tagName === 'IFRAME') {
if (!['presentation', 'none'].includes(role) || tagName === 'IFRAME' || tagName === 'FRAME') {
options.visitedElements.add(element);
const title = element.getAttribute('title') || '';
if (trimFlatString(title))
Expand Down
2 changes: 1 addition & 1 deletion packages/injected/src/selectorGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ function buildNoTextCandidates(injectedScript: InjectedScript, element: Element,
candidates.push({ engine: 'css', selector: escapeNodeName(element), score: kCSSTagNameScore });
}

if (element.nodeName === 'IFRAME') {
if (element.nodeName === 'IFRAME' || element.nodeName === 'FRAME') {
for (const attribute of ['name', 'title']) {
if (element.getAttribute(attribute))
candidates.push({ engine: 'css', selector: `${escapeNodeName(element)}[${attribute}=${quoteCSSAttributeValue(element.getAttribute(attribute)!)}]`, score: kIframeByAttributeScore });
Expand Down
9 changes: 5 additions & 4 deletions packages/playwright-core/src/server/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1111,13 +1111,13 @@ export class InitScript extends DisposableObject {
}
}

export async function ariaSnapshotForFrame(progress: Progress, frame: frames.Frame, selector: string | undefined, options: { mode?: 'ai' | 'default', doNotRenderActive?: boolean, depth?: number, boxes?: boolean } = {}): Promise<string[]> {
export async function ariaSnapshotForFrame(progress: Progress, frame: frames.Frame, selector: string | undefined, options: { mode?: 'ai' | 'default', doNotRenderActive?: boolean, depth?: number, boxes?: boolean, strict?: boolean } = {}): Promise<string[]> {
const snapshot = await frame.retryWithProgressAndTimeouts(progress, [1000, 2000, 4000, 8000], async (progress, continuePolling) => {
try {
// Note: the resolved frame might differ from the original |frame|.
// See https://developer.mozilla.org/en-US/docs/Web/API/Document/body for body/frameset explanation.
// Non-strict, because pages with nested framesets have multiple "frameset" elements.
const resolved = await progress.race(frame.selectors.callOnSelector(selector || 'body,frameset', { strict: !!selector }, ({ injected, elements }, ariaOptions) => {
const resolved = await progress.race(frame.selectors.callOnSelector(selector || 'body,frameset', { strict: options.strict ?? !!selector }, ({ injected, elements }, ariaOptions) => {
return injected.ariaSnapshotWithRefs(elements[0], ariaOptions);
}, {
mode: options.mode ?? 'default',
Expand All @@ -1144,9 +1144,10 @@ export async function ariaSnapshotForFrame(progress: Progress, frame: frames.Fra
progress.setAllowConcurrentOrNestedRaces(true);
const childSnapshotPromises = renderedIframeRefs.map(async ref => {
const childDepth = options.depth ? options.depth - snapshot.iframeDepths[ref] - 1 : undefined;
const frameBodySelector = `aria-ref=${ref} >> internal:control=enter-frame >> body`;
// Non-strict, because child frameset documents have multiple "frameset" elements.
const frameRootSelector = `aria-ref=${ref} >> internal:control=enter-frame >> body,frameset`;
try {
return await ariaSnapshotForFrame(progress, snapshot.resolvedFrame, frameBodySelector, { ...options, depth: childDepth });
return await ariaSnapshotForFrame(progress, snapshot.resolvedFrame, frameRootSelector, { ...options, depth: childDepth, strict: false });
} catch {
return [];
}
Expand Down
88 changes: 85 additions & 3 deletions tests/page/page-aria-snapshot-ai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -668,10 +668,92 @@ it('should support many properties on iframes', async ({ page }) => {
`);
});

it('should not timeout on frameset pages', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41784' } }, async ({ page, server }) => {
await page.goto(server.PREFIX + '/frames/frameset.html');
it('should snapshot frameset pages', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41784' } }, async ({ page, server }) => {
server.setRoute('/frameset.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<frameset rows="50%,50%"><frameset cols="50%,50%"><frame src="/frame-one.html"><frame src="/frame-two.html"></frameset><frame src="/frame-three.html"></frameset>`);
});
for (const name of ['one', 'two', 'three']) {
server.setRoute(`/frame-${name}.html`, (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<button>Button ${name}</button>`);
});
}
await page.goto(server.PREFIX + '/frameset.html');

const snapshot = await snapshotForAI(page, { timeout: 3000 });
expect(snapshot).toBe('');
expect(snapshot).toContainYaml(`
- generic [active] [ref=e1]:
- generic [ref=e2]:
- iframe [ref=e3]:
- button "Button one" [ref=f1e2]
- iframe [ref=e4]:
- button "Button two" [ref=f2e2]
- iframe [ref=e5]:
- button "Button three" [ref=f3e2]
`);
await expect(page.locator('aria-ref=f2e2')).toHaveText('Button two');
});

it('should snapshot a locator inside a frameset frame', async ({ page, server }) => {
await page.goto(server.PREFIX + '/frames/frameset.html');

const snapshot = await page.frames()[1].locator('body').ariaSnapshot({ mode: 'ai' });
expect(snapshot).toContainYaml(`
- generic [ref=f1e1]: Hi, I'm frame
`);

await expect(page.locator('aria-ref=f1e1')).toHaveText(`Hi, I'm frame`);
const resolved = await page.locator('aria-ref=f1e1').normalize();
expect(resolved.toString()).toBe(`locator('frame').first().contentFrame().locator('body')`);
});

it('should stitch iframes inside a frameset frame', async ({ page, server }) => {
server.setRoute('/frameset-with-iframe.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<frameset><frame src="/frame-with-iframe.html"></frameset>`);
});
server.setRoute('/frame-with-iframe.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<button>In frame</button><iframe srcdoc="<button>In iframe</button>"></iframe>`);
});
await page.goto(server.PREFIX + '/frameset-with-iframe.html');

expect(await snapshotForAI(page)).toContainYaml(`
- iframe [ref=e2]:
- generic [ref=f1e1]:
- button "In frame" [ref=f1e2]
- iframe [ref=f1e3]:
- button "In iframe" [ref=f2e2]
`);
await expect(page.locator('aria-ref=f1e2')).toHaveText('In frame');
await expect(page.locator('aria-ref=f2e2')).toHaveText('In iframe');

const resolved = await page.locator('aria-ref=f2e2').normalize();
expect(resolved.toString()).toBe(`locator('frame').contentFrame().locator('iframe').contentFrame().getByRole('button', { name: 'In iframe' })`);
});

it('should stitch nested frameset documents', async ({ page, server }) => {
server.setRoute('/outer-frameset.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<frameset><frame src="/inner-frameset.html"></frameset>`);
});
server.setRoute('/inner-frameset.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<frameset><frameset><frame src="/leaf.html"></frameset></frameset>`);
});
server.setRoute('/leaf.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<button>Leaf button</button>`);
});
await page.goto(server.PREFIX + '/outer-frameset.html');

expect(await snapshotForAI(page)).toContainYaml(`
- iframe [ref=e2]:
- iframe [ref=f1e3]:
- button "Leaf button" [ref=f2e2]
`);
await expect(page.locator('aria-ref=f2e2')).toHaveText('Leaf button');
});

it('should collapse inline generic nodes', async ({ page }) => {
Expand Down
20 changes: 20 additions & 0 deletions tests/page/page-aria-snapshot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,26 @@ it('should snapshot a locator inside an iframe', async ({ page }) => {
`);
});

it('should include frames on frameset pages', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41784' } }, async ({ page, server }) => {
server.setRoute('/frameset.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<frameset cols="50%,50%"><frame src="/frames/frame.html"><frame src="/frames/frame.html"></frameset>`);
});
await page.goto(server.PREFIX + '/frameset.html');
// Like iframes, frames are listed in the snapshot, but their content is not included.
expect(await page.ariaSnapshot({ timeout: 3000 })).toBe(unshift(`
- iframe
- iframe
`));
});

it('should snapshot a locator inside a frameset frame', async ({ page, server }) => {
await page.goto(server.PREFIX + '/frames/frameset.html');
await checkAndMatchSnapshot(page.frameLocator('frame').first().locator('body'), `
- text: Hi, I'm frame
`);
});

it('should snapshot with box from page', async ({ page }) => {
await page.setContent(`
<button style="position:absolute;left:100px;top:50px;width:80px;height:40px;margin:0;padding:0;border:0;">click</button>
Expand Down
Loading