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 { escapeRegExp, longestCommonSubstring, normalizeWhiteSpace } from '@isomorphic/stringUtils';
|
||||
|
||||
import { getElementComputedStyle, getGlobalOptions } from './domUtils';
|
||||
import { box, getElementComputedStyle, getGlobalOptions } from './domUtils';
|
||||
import * as roleUtils from './roleUtils';
|
||||
import { yamlEscapeKeyIfNeeded, yamlEscapeValueIfNeeded } from './yaml';
|
||||
|
||||
import type { AriaProps, AriaRegex, AriaRole, AriaTemplateNode, AriaTemplateRoleNode, AriaTemplateTextNode } from '@isomorphic/ariaSnapshot';
|
||||
import type { Box } from './domUtils';
|
||||
|
||||
export type AriaNode = AriaProps & {
|
||||
role: AriaRole | 'fragment' | 'iframe';
|
||||
name: string;
|
||||
children: (AriaNode | string)[];
|
||||
element: Element;
|
||||
box: Box;
|
||||
props: Record<string, string>;
|
||||
};
|
||||
|
||||
@ -42,7 +44,7 @@ export function generateAriaTree(rootElement: Element, generation: number, optio
|
||||
const visited = new Set<Node>();
|
||||
|
||||
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>(),
|
||||
generation,
|
||||
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 {
|
||||
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 role = roleUtils.getAriaRole(element) ?? defaultRole;
|
||||
@ -155,7 +157,7 @@ function toAriaNode(element: Element, options?: { emitGeneric?: boolean }): Aria
|
||||
return null;
|
||||
|
||||
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))
|
||||
result.checked = roleUtils.getAriaChecked(element);
|
||||
@ -405,7 +407,7 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, options?: { mode?: 'r
|
||||
key += ` [pressed]`;
|
||||
if (ariaNode.selected === true)
|
||||
key += ` [selected]`;
|
||||
if (options?.ref) {
|
||||
if (options?.ref && ariaNode.box.visible) {
|
||||
const id = ariaSnapshot.ids.get(ariaNode.element);
|
||||
if (id)
|
||||
key += ` [ref=s${ariaSnapshot.generation}e${id}]`;
|
||||
|
@ -104,25 +104,35 @@ export function isElementStyleVisibilityVisible(element: Element, style?: CSSSty
|
||||
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.
|
||||
const style = getElementComputedStyle(element);
|
||||
if (!style)
|
||||
return true;
|
||||
return { visible: true };
|
||||
if (style.display === 'contents') {
|
||||
// display:contents is not rendered itself, but its child nodes are.
|
||||
for (let child = element.firstChild; child; child = child.nextSibling) {
|
||||
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))
|
||||
return true;
|
||||
return { visible: true, style };
|
||||
}
|
||||
return false;
|
||||
return { visible: false, style };
|
||||
}
|
||||
if (!isElementStyleVisibilityVisible(element, style))
|
||||
return false;
|
||||
return { style, visible: false };
|
||||
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) {
|
||||
|
@ -731,6 +731,19 @@ it('ref mode can be used to stitch all frame snapshots', async ({ page, server }
|
||||
`.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 }) => {
|
||||
await page.setContent(`
|
||||
<style>
|
||||
@ -742,21 +755,36 @@ it('emit generic roles for nodes w/o roles', async ({ page }) => {
|
||||
</style>
|
||||
<div>
|
||||
<label>
|
||||
<span>
|
||||
<input type="radio" value="Apple" checked="">
|
||||
</span>
|
||||
<span>Apple</span>
|
||||
</label>
|
||||
<label>
|
||||
<span>
|
||||
<input type="radio" value="Pear">
|
||||
</span>
|
||||
<span>Pear</span>
|
||||
</label>
|
||||
<label>
|
||||
<span>
|
||||
<input type="radio" value="Orange">
|
||||
</span>
|
||||
<span>Orange</span>
|
||||
</label>
|
||||
</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:
|
||||
- generic: Apple
|
||||
- generic: Pear
|
||||
- generic: Orange`);
|
||||
expect(snapshot).toContain(`- generic [ref=s1e3]:
|
||||
- generic [ref=s1e4]:
|
||||
- radio "Apple" [checked]
|
||||
- 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