mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore(aria): do not generate refs for invisible elements (#35694)
This commit is contained in:
parent
8e8f8635f2
commit
85eeb37c05
@ -17,17 +17,19 @@
|
|||||||
import { Map, Set } from '@isomorphic/builtins';
|
import { Map, Set } from '@isomorphic/builtins';
|
||||||
import { escapeRegExp, longestCommonSubstring, normalizeWhiteSpace } from '@isomorphic/stringUtils';
|
import { escapeRegExp, longestCommonSubstring, normalizeWhiteSpace } from '@isomorphic/stringUtils';
|
||||||
|
|
||||||
import { getElementComputedStyle, getGlobalOptions } from './domUtils';
|
import { box, getElementComputedStyle, getGlobalOptions } from './domUtils';
|
||||||
import * as roleUtils from './roleUtils';
|
import * as roleUtils from './roleUtils';
|
||||||
import { yamlEscapeKeyIfNeeded, yamlEscapeValueIfNeeded } from './yaml';
|
import { yamlEscapeKeyIfNeeded, yamlEscapeValueIfNeeded } from './yaml';
|
||||||
|
|
||||||
import type { AriaProps, AriaRegex, AriaRole, AriaTemplateNode, AriaTemplateRoleNode, AriaTemplateTextNode } from '@isomorphic/ariaSnapshot';
|
import type { AriaProps, AriaRegex, AriaRole, AriaTemplateNode, AriaTemplateRoleNode, AriaTemplateTextNode } from '@isomorphic/ariaSnapshot';
|
||||||
|
import type { Box } from './domUtils';
|
||||||
|
|
||||||
export type AriaNode = AriaProps & {
|
export type AriaNode = AriaProps & {
|
||||||
role: AriaRole | 'fragment' | 'iframe';
|
role: AriaRole | 'fragment' | 'iframe';
|
||||||
name: string;
|
name: string;
|
||||||
children: (AriaNode | string)[];
|
children: (AriaNode | string)[];
|
||||||
element: Element;
|
element: Element;
|
||||||
|
box: Box;
|
||||||
props: Record<string, string>;
|
props: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -42,7 +44,7 @@ export function generateAriaTree(rootElement: Element, generation: number, optio
|
|||||||
const visited = new Set<Node>();
|
const visited = new Set<Node>();
|
||||||
|
|
||||||
const snapshot: AriaSnapshot = {
|
const snapshot: AriaSnapshot = {
|
||||||
root: { role: 'fragment', name: '', children: [], element: rootElement, props: {} },
|
root: { role: 'fragment', name: '', children: [], element: rootElement, props: {}, box: box(rootElement) },
|
||||||
elements: new Map<number, Element>(),
|
elements: new Map<number, Element>(),
|
||||||
generation,
|
generation,
|
||||||
ids: new Map<Element, number>(),
|
ids: new Map<Element, number>(),
|
||||||
@ -147,7 +149,7 @@ export function generateAriaTree(rootElement: Element, generation: number, optio
|
|||||||
|
|
||||||
function toAriaNode(element: Element, options?: { emitGeneric?: boolean }): AriaNode | null {
|
function toAriaNode(element: Element, options?: { emitGeneric?: boolean }): AriaNode | null {
|
||||||
if (element.nodeName === 'IFRAME')
|
if (element.nodeName === 'IFRAME')
|
||||||
return { role: 'iframe', name: '', children: [], props: {}, element };
|
return { role: 'iframe', name: '', children: [], props: {}, element, box: box(element) };
|
||||||
|
|
||||||
const defaultRole = options?.emitGeneric ? 'generic' : null;
|
const defaultRole = options?.emitGeneric ? 'generic' : null;
|
||||||
const role = roleUtils.getAriaRole(element) ?? defaultRole;
|
const role = roleUtils.getAriaRole(element) ?? defaultRole;
|
||||||
@ -155,7 +157,7 @@ function toAriaNode(element: Element, options?: { emitGeneric?: boolean }): Aria
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
const name = normalizeWhiteSpace(roleUtils.getElementAccessibleName(element, false) || '');
|
const name = normalizeWhiteSpace(roleUtils.getElementAccessibleName(element, false) || '');
|
||||||
const result: AriaNode = { role, name, children: [], props: {}, element };
|
const result: AriaNode = { role, name, children: [], props: {}, element, box: box(element) };
|
||||||
|
|
||||||
if (roleUtils.kAriaCheckedRoles.includes(role))
|
if (roleUtils.kAriaCheckedRoles.includes(role))
|
||||||
result.checked = roleUtils.getAriaChecked(element);
|
result.checked = roleUtils.getAriaChecked(element);
|
||||||
@ -405,7 +407,7 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, options?: { mode?: 'r
|
|||||||
key += ` [pressed]`;
|
key += ` [pressed]`;
|
||||||
if (ariaNode.selected === true)
|
if (ariaNode.selected === true)
|
||||||
key += ` [selected]`;
|
key += ` [selected]`;
|
||||||
if (options?.ref) {
|
if (options?.ref && ariaNode.box.visible) {
|
||||||
const id = ariaSnapshot.ids.get(ariaNode.element);
|
const id = ariaSnapshot.ids.get(ariaNode.element);
|
||||||
if (id)
|
if (id)
|
||||||
key += ` [ref=s${ariaSnapshot.generation}e${id}]`;
|
key += ` [ref=s${ariaSnapshot.generation}e${id}]`;
|
||||||
|
@ -104,25 +104,35 @@ export function isElementStyleVisibilityVisible(element: Element, style?: CSSSty
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isElementVisible(element: Element): boolean {
|
export type Box = {
|
||||||
|
visible: boolean;
|
||||||
|
rect?: DOMRect;
|
||||||
|
style?: CSSStyleDeclaration;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function box(element: Element): Box {
|
||||||
// Note: this logic should be similar to waitForDisplayedAtStablePosition() to avoid surprises.
|
// Note: this logic should be similar to waitForDisplayedAtStablePosition() to avoid surprises.
|
||||||
const style = getElementComputedStyle(element);
|
const style = getElementComputedStyle(element);
|
||||||
if (!style)
|
if (!style)
|
||||||
return true;
|
return { visible: true };
|
||||||
if (style.display === 'contents') {
|
if (style.display === 'contents') {
|
||||||
// display:contents is not rendered itself, but its child nodes are.
|
// display:contents is not rendered itself, but its child nodes are.
|
||||||
for (let child = element.firstChild; child; child = child.nextSibling) {
|
for (let child = element.firstChild; child; child = child.nextSibling) {
|
||||||
if (child.nodeType === 1 /* Node.ELEMENT_NODE */ && isElementVisible(child as Element))
|
if (child.nodeType === 1 /* Node.ELEMENT_NODE */ && isElementVisible(child as Element))
|
||||||
return true;
|
return { visible: true, style };
|
||||||
if (child.nodeType === 3 /* Node.TEXT_NODE */ && isVisibleTextNode(child as Text))
|
if (child.nodeType === 3 /* Node.TEXT_NODE */ && isVisibleTextNode(child as Text))
|
||||||
return true;
|
return { visible: true, style };
|
||||||
}
|
}
|
||||||
return false;
|
return { visible: false, style };
|
||||||
}
|
}
|
||||||
if (!isElementStyleVisibilityVisible(element, style))
|
if (!isElementStyleVisibilityVisible(element, style))
|
||||||
return false;
|
return { style, visible: false };
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
return rect.width > 0 && rect.height > 0;
|
return { rect, style, visible: rect.width > 0 && rect.height > 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isElementVisible(element: Element): boolean {
|
||||||
|
return box(element).visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isVisibleTextNode(node: Text) {
|
export function isVisibleTextNode(node: Text) {
|
||||||
|
@ -731,6 +731,19 @@ it('ref mode can be used to stitch all frame snapshots', async ({ page, server }
|
|||||||
`.trim());
|
`.trim());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not include hidden input elements', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<button>One</button>
|
||||||
|
<button style="width: 0; height: 0; appearance: none; border: 0; padding: 0;">Two</button>
|
||||||
|
<button>Three</button>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const snapshot = await page.locator('body').ariaSnapshot({ ref: true });
|
||||||
|
expect(snapshot).toContain(`- button "One" [ref=s1e3]
|
||||||
|
- button "Two"
|
||||||
|
- button "Three" [ref=s1e5]`);
|
||||||
|
});
|
||||||
|
|
||||||
it('emit generic roles for nodes w/o roles', async ({ page }) => {
|
it('emit generic roles for nodes w/o roles', async ({ page }) => {
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
<style>
|
<style>
|
||||||
@ -742,21 +755,36 @@ it('emit generic roles for nodes w/o roles', async ({ page }) => {
|
|||||||
</style>
|
</style>
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<label>
|
||||||
|
<span>
|
||||||
|
<input type="radio" value="Apple" checked="">
|
||||||
|
</span>
|
||||||
<span>Apple</span>
|
<span>Apple</span>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
|
<span>
|
||||||
|
<input type="radio" value="Pear">
|
||||||
|
</span>
|
||||||
<span>Pear</span>
|
<span>Pear</span>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
|
<span>
|
||||||
|
<input type="radio" value="Orange">
|
||||||
|
</span>
|
||||||
<span>Orange</span>
|
<span>Orange</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const snapshot = await page.locator('body').ariaSnapshot({ emitGeneric: true });
|
const snapshot = await page.locator('body').ariaSnapshot({ ref: true, emitGeneric: true });
|
||||||
|
|
||||||
expect(snapshot).toContain(`- generic:
|
expect(snapshot).toContain(`- generic [ref=s1e3]:
|
||||||
- generic: Apple
|
- generic [ref=s1e4]:
|
||||||
- generic: Pear
|
- radio "Apple" [checked]
|
||||||
- generic: Orange`);
|
- text: Apple
|
||||||
|
- generic [ref=s1e8]:
|
||||||
|
- radio "Pear"
|
||||||
|
- text: Pear
|
||||||
|
- generic [ref=s1e12]:
|
||||||
|
- radio "Orange"
|
||||||
|
- text: Orange`);
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user