feat: include iframes in aria snapshots with `ref` (#35396)

This commit is contained in:
Simon Knott 2025-03-28 12:46:20 +01:00 committed by GitHub
parent 2c0e1e5e3a
commit e3bb687cfc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 58 additions and 8 deletions

View File

@ -24,7 +24,7 @@ import type { AriaProps, AriaRegex, AriaRole, AriaTemplateNode, AriaTemplateRole
import type { Builtins } from '../isomorphic/builtins';
export type AriaNode = AriaProps & {
role: AriaRole | 'fragment';
role: AriaRole | 'fragment' | 'iframe';
name: string;
children: (AriaNode | string)[];
element: Element;
@ -38,7 +38,7 @@ export type AriaSnapshot = {
ids: Builtins.Map<Element, number>;
};
export function generateAriaTree(builtins: Builtins, rootElement: Element, generation: number): AriaSnapshot {
export function generateAriaTree(builtins: Builtins, rootElement: Element, generation: number, includeIframe: boolean): AriaSnapshot {
const visited = new builtins.Set<Node>();
const snapshot: AriaSnapshot = {
@ -87,7 +87,7 @@ export function generateAriaTree(builtins: Builtins, rootElement: Element, gener
}
addElement(element);
const childAriaNode = toAriaNode(builtins, element);
const childAriaNode = toAriaNode(builtins, element, includeIframe);
if (childAriaNode)
ariaNode.children.push(childAriaNode);
processElement(childAriaNode || ariaNode, element, ariaChildren);
@ -144,7 +144,10 @@ export function generateAriaTree(builtins: Builtins, rootElement: Element, gener
return snapshot;
}
function toAriaNode(builtins: Builtins, element: Element): AriaNode | null {
function toAriaNode(builtins: Builtins, element: Element, includeIframe: boolean): AriaNode | null {
if (includeIframe && element.nodeName === 'IFRAME')
return { role: 'iframe', name: '', children: [], props: {}, element };
const role = roleUtils.getAriaRole(element);
if (!role || role === 'presentation' || role === 'none')
return null;
@ -232,7 +235,7 @@ export type MatcherReceived = {
};
export function matchesAriaTree(builtins: Builtins, rootElement: Element, template: AriaTemplateNode): { matches: AriaNode[], received: MatcherReceived } {
const snapshot = generateAriaTree(builtins, rootElement, 0);
const snapshot = generateAriaTree(builtins, rootElement, 0, false);
const matches = matchesNodeDeep(snapshot.root, template, false);
return {
matches,
@ -244,7 +247,7 @@ export function matchesAriaTree(builtins: Builtins, rootElement: Element, templa
}
export function getAllByAria(builtins: Builtins, rootElement: Element, template: AriaTemplateNode): Element[] {
const root = generateAriaTree(builtins, rootElement, 0).root;
const root = generateAriaTree(builtins, rootElement, 0, false).root;
const matches = matchesNodeDeep(root, template, true);
return matches.map(n => n.element);
}

View File

@ -285,7 +285,7 @@ export class InjectedScript {
if (node.nodeType !== Node.ELEMENT_NODE)
throw this.createStacklessError('Can only capture aria snapshot of Element nodes.');
const generation = (this._lastAriaSnapshot?.generation || 0) + 1;
this._lastAriaSnapshot = generateAriaTree(this.builtins, node as Element, generation);
this._lastAriaSnapshot = generateAriaTree(this.builtins, node as Element, generation, options?.ref ?? false);
return renderAriaTree(this._lastAriaSnapshot, options);
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import type { Locator } from '@playwright/test';
import type { Locator, FrameLocator, Page } from '@playwright/test';
import { test as it, expect } from './pageTest';
function unshift(snapshot: string): string {
@ -677,3 +677,50 @@ it('should generate refs', async ({ page }) => {
const e = await expect(page.locator('aria-ref=s1e3')).toHaveText('One').catch(e => e);
expect(e.message).toContain('Error: Stale aria-ref, expected s2e{number}, got s1e3');
});
it('ref mode should list iframes', async ({ page }) => {
await page.setContent(`
<h1>Hello</h1>
<iframe name="foo" src="data:text/html,<h1>World</h1>">
`);
const snapshot1 = await page.locator('body').ariaSnapshot({ ref: true });
expect(snapshot1).toContain('- iframe [ref=s1e4]');
const frameSnapshot = await page.frameLocator(`aria-ref=s1e4`).locator('body').ariaSnapshot({ ref: true });
expect(frameSnapshot).toEqual('- heading "World" [level=1] [ref=s1e3]');
});
it('ref mode can be used to stitch all frame snapshots', async ({ page, server }) => {
await page.goto(server.PREFIX + '/frames/nested-frames.html');
async function allFrameSnapshot(frame: Page | FrameLocator): Promise<string> {
const snapshot = await frame.locator('body').ariaSnapshot({ ref: true });
const lines = snapshot.split('\n');
const result = [];
for (const line of lines) {
const match = line.match(/^(\s*)- iframe \[ref=(.*)\]/);
if (!match) {
result.push(line);
continue;
}
const leadingSpace = match[1];
const ref = match[2];
const childFrame = frame.frameLocator(`aria-ref=${ref}`);
const childSnapshot = await allFrameSnapshot(childFrame);
result.push(line + ':', childSnapshot.split('\n').map(l => leadingSpace + ' ' + l).join('\n'));
}
return result.join('\n');
}
expect(await allFrameSnapshot(page)).toEqual(`
- iframe [ref=s1e3]:
- iframe [ref=s1e3]:
- text: Hi, I'm frame
- iframe [ref=s1e4]:
- text: Hi, I'm frame
- iframe [ref=s1e4]:
- text: Hi, I'm frame
`.trim());
});