diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index 9c87d38fc1..eb3db98855 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -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; }; @@ -42,7 +44,7 @@ export function generateAriaTree(rootElement: Element, generation: number, optio const visited = new Set(); 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(), generation, ids: new Map(), @@ -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}]`; diff --git a/packages/injected/src/domUtils.ts b/packages/injected/src/domUtils.ts index a45110231e..eb97e04a8b 100644 --- a/packages/injected/src/domUtils.ts +++ b/packages/injected/src/domUtils.ts @@ -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) { diff --git a/tests/page/page-aria-snapshot.spec.ts b/tests/page/page-aria-snapshot.spec.ts index 809d419eec..00081b7bc8 100644 --- a/tests/page/page-aria-snapshot.spec.ts +++ b/tests/page/page-aria-snapshot.spec.ts @@ -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(` + + + + `); + + 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(`
`); - 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`); });