From 1617842a1e238bb8fea1c09188d9e14f45cf0536 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Wed, 30 Jul 2025 09:48:34 -0600 Subject: [PATCH 1/6] fix(idref): fallback to qsa --- lib/commons/dom/idrefs.js | 38 ++-- lib/commons/text/accessible-text.js | 6 +- test/commons/dom/idrefs.js | 70 ++++--- test/integration/virtual-rules/select-name.js | 190 ++++++++++++++---- 4 files changed, 215 insertions(+), 89 deletions(-) diff --git a/lib/commons/dom/idrefs.js b/lib/commons/dom/idrefs.js index be3dc56db0..c9688689c3 100644 --- a/lib/commons/dom/idrefs.js +++ b/lib/commons/dom/idrefs.js @@ -1,5 +1,10 @@ import getRootNode from './get-root-node'; -import { tokenList } from '../../core/utils'; +import { + tokenList, + nodeLookup, + closest, + querySelectorAll +} from '../../core/utils'; /** * Get elements referenced via a space-separated token attribute; @@ -18,24 +23,29 @@ import { tokenList } from '../../core/utils'; * */ function idrefs(node, attr) { - node = node.actualNode || node; + const { vNode, domNode } = nodeLookup(node); + const result = []; + let doc; + const attrValue = vNode?.attr(attr) ?? node.getAttribute(attr); - try { - const doc = getRootNode(node); - const result = []; - let attrValue = node.getAttribute(attr); + if (attrValue) { + for (const token of tokenList(attrValue)) { + try { + doc ??= getRootNode(domNode); + result.push(doc.getElementById(token)); + } catch { + // don't run QSA on detached nodes or partial trees + const root = closest(vNode, 'html'); + if (!root) { + throw new TypeError('Cannot resolve id references for non-DOM nodes'); + } - if (attrValue) { - attrValue = tokenList(attrValue); - for (let index = 0; index < attrValue.length; index++) { - result.push(doc.getElementById(attrValue[index])); + result.push(querySelectorAll(root, `#${token}`, vNode.shadowId)?.[0]); } } - - return result; - } catch { - throw new TypeError('Cannot resolve id references for non-DOM nodes'); } + + return result; } export default idrefs; diff --git a/lib/commons/text/accessible-text.js b/lib/commons/text/accessible-text.js index 3dfb04157a..711d6d8beb 100644 --- a/lib/commons/text/accessible-text.js +++ b/lib/commons/text/accessible-text.js @@ -1,5 +1,5 @@ import accessibleTextVirtual from './accessible-text-virtual'; -import { getNodeFromTree } from '../../core/utils'; +import { nodeLookup } from '../../core/utils'; /** * Finds virtual node and calls accessibleTextVirtual() @@ -12,8 +12,8 @@ import { getNodeFromTree } from '../../core/utils'; * @return {string} */ function accessibleText(element, context) { - const virtualNode = getNodeFromTree(element); // throws an exception on purpose if axe._tree not correct - return accessibleTextVirtual(virtualNode, context); + const { vNode } = nodeLookup(element); // throws an exception on purpose if axe._tree not correct + return accessibleTextVirtual(vNode, context); } export default accessibleText; diff --git a/test/commons/dom/idrefs.js b/test/commons/dom/idrefs.js index b187612232..8f5abc6d0c 100644 --- a/test/commons/dom/idrefs.js +++ b/test/commons/dom/idrefs.js @@ -1,40 +1,36 @@ function createContentIDR() { 'use strict'; - var group = document.createElement('div'); + const group = document.createElement('div'); group.id = 'target'; return group; } function makeShadowTreeIDR(node) { 'use strict'; - var root = node.attachShadow({ mode: 'open' }); - var div = document.createElement('div'); + const root = node.attachShadow({ mode: 'open' }); + const div = document.createElement('div'); div.className = 'parent'; div.setAttribute('target', 'target'); root.appendChild(div); div.appendChild(createContentIDR()); } -describe('dom.idrefs', function () { +describe('dom.idrefs', () => { 'use strict'; - var fixture = document.getElementById('fixture'); - var shadowSupported = axe.testUtils.shadowSupport.v1; + const fixture = document.getElementById('fixture'); + const shadowSupported = axe.testUtils.shadowSupport.v1; - afterEach(function () { - fixture.innerHTML = ''; - }); - - it('should find referenced nodes by ID', function () { + it('should find referenced nodes by ID', () => { fixture.innerHTML = '
' + '
'; - var start = document.getElementById('start'), - expected = [ - document.getElementById('target1'), - document.getElementById('target2') - ]; + const start = document.getElementById('start'); + const expected = [ + document.getElementById('target1'), + document.getElementById('target2') + ]; assert.deepEqual( axe.commons.dom.idrefs(start, 'aria-cats'), @@ -45,13 +41,13 @@ describe('dom.idrefs', function () { (shadowSupported ? it : xit)( 'should find only referenced nodes within the current root: shadow DOM', - function () { + () => { // shadow DOM v1 - note: v0 is compatible with this code, so no need // to specifically test this fixture.innerHTML = '
'; makeShadowTreeIDR(fixture.firstChild); - var start = fixture.firstChild.shadowRoot.querySelector('.parent'); - var expected = [fixture.firstChild.shadowRoot.getElementById('target')]; + const start = fixture.firstChild.shadowRoot.querySelector('.parent'); + const expected = [fixture.firstChild.shadowRoot.getElementById('target')]; assert.deepEqual( axe.commons.dom.idrefs(start, 'target'), @@ -63,14 +59,14 @@ describe('dom.idrefs', function () { (shadowSupported ? it : xit)( 'should find only referenced nodes within the current root: document', - function () { + () => { // shadow DOM v1 - note: v0 is compatible with this code, so no need // to specifically test this fixture.innerHTML = '
'; makeShadowTreeIDR(fixture.firstChild); - var start = fixture.querySelector('.parent'); - var expected = [document.getElementById('target')]; + const start = fixture.querySelector('.parent'); + const expected = [document.getElementById('target')]; assert.deepEqual( axe.commons.dom.idrefs(start, 'target'), @@ -80,17 +76,17 @@ describe('dom.idrefs', function () { } ); - it('should insert null if a reference is not found', function () { + it('should insert null if a reference is not found', () => { fixture.innerHTML = '
' + '
'; - var start = document.getElementById('start'), - expected = [ - document.getElementById('target1'), - document.getElementById('target2'), - null - ]; + const start = document.getElementById('start'); + const expected = [ + document.getElementById('target1'), + document.getElementById('target2'), + null + ]; assert.deepEqual( axe.commons.dom.idrefs(start, 'aria-cats'), @@ -99,17 +95,17 @@ describe('dom.idrefs', function () { ); }); - it('should not fail when extra whitespace is used', function () { + it('should not fail when extra whitespace is used', () => { fixture.innerHTML = '
' + '
'; - var start = document.getElementById('start'), - expected = [ - document.getElementById('target1'), - document.getElementById('target2'), - null - ]; + const start = document.getElementById('start'); + const expected = [ + document.getElementById('target1'), + document.getElementById('target2'), + null + ]; assert.deepEqual( axe.commons.dom.idrefs(start, 'aria-cats'), @@ -117,4 +113,6 @@ describe('dom.idrefs', function () { 'Should find it!' ); }); + + // virtual-node tests test throwing for non-DOM nodes and working with complete trees }); diff --git a/test/integration/virtual-rules/select-name.js b/test/integration/virtual-rules/select-name.js index 97783b389e..8e2f71c59c 100644 --- a/test/integration/virtual-rules/select-name.js +++ b/test/integration/virtual-rules/select-name.js @@ -1,6 +1,6 @@ -describe('select-name virtual-rule', function () { - it('should pass for aria-label', function () { - var results = axe.runVirtualRule('select-name', { +describe('select-name virtual-rule', () => { + it('should pass for aria-label', () => { + const results = axe.runVirtualRule('select-name', { nodeName: 'select', attributes: { 'aria-label': 'foobar' @@ -12,8 +12,8 @@ describe('select-name virtual-rule', function () { assert.lengthOf(results.incomplete, 0); }); - it('should incomplete for aria-labelledby', function () { - var results = axe.runVirtualRule('select-name', { + it('should incomplete for aria-labelledby', () => { + const results = axe.runVirtualRule('select-name', { nodeName: 'select', attributes: { 'aria-labelledby': 'foobar' @@ -25,14 +25,59 @@ describe('select-name virtual-rule', function () { assert.lengthOf(results.incomplete, 1); }); - it('should pass for implicit label', function () { - var node = new axe.SerialVirtualNode({ + it('should pass for aria-labelledby in complete tree', () => { + const html = new axe.SerialVirtualNode({ + nodeName: 'html' + }); + const body = new axe.SerialVirtualNode({ + nodeName: 'body' + }); + const div = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + id: 'foobar' + }, + id: 'foobar' + }); + const text = new axe.SerialVirtualNode({ + nodeName: '#text', + nodeType: 3, + nodeValue: 'foobar' + }); + const select = new axe.SerialVirtualNode({ + nodeName: 'select', + attributes: { + 'aria-labelledby': 'foobar' + } + }); + html.parent = null; + html.children = [body]; + + body.parent = html; + body.children = [div, select]; + + div.parent = body; + div.children = [text]; + + text.parent = div; + + select.parent = body; + + const results = axe.runVirtualRule('select-name', select); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should pass for implicit label', () => { + const node = new axe.SerialVirtualNode({ nodeName: 'select' }); - var parent = new axe.SerialVirtualNode({ + const parent = new axe.SerialVirtualNode({ nodeName: 'label' }); - var child = new axe.SerialVirtualNode({ + const child = new axe.SerialVirtualNode({ nodeName: '#text', nodeType: 3, nodeValue: 'foobar' @@ -40,30 +85,30 @@ describe('select-name virtual-rule', function () { node.parent = parent; parent.children = [child, node]; - var results = axe.runVirtualRule('select-name', node); + const results = axe.runVirtualRule('select-name', node); assert.lengthOf(results.passes, 1); assert.lengthOf(results.violations, 0); assert.lengthOf(results.incomplete, 0); }); - it('should incomplete for explicit label', function () { - var node = new axe.SerialVirtualNode({ + it('should incomplete for explicit label', () => { + const node = new axe.SerialVirtualNode({ nodeName: 'select', attributes: { id: 'foobar' } }); - var results = axe.runVirtualRule('select-name', node); + const results = axe.runVirtualRule('select-name', node); assert.lengthOf(results.passes, 0); assert.lengthOf(results.violations, 0); assert.lengthOf(results.incomplete, 1); }); - it('should pass for title', function () { - var results = axe.runVirtualRule('select-name', { + it('should pass for title', () => { + const results = axe.runVirtualRule('select-name', { nodeName: 'select', attributes: { title: 'foobar' @@ -75,8 +120,8 @@ describe('select-name virtual-rule', function () { assert.lengthOf(results.incomplete, 0); }); - it('should pass for role=presentation when disabled', function () { - var results = axe.runVirtualRule('select-name', { + it('should pass for role=presentation when disabled', () => { + const results = axe.runVirtualRule('select-name', { nodeName: 'select', attributes: { role: 'presentation', @@ -89,8 +134,8 @@ describe('select-name virtual-rule', function () { assert.lengthOf(results.incomplete, 0); }); - it('should pass for role=none when disabled', function () { - var results = axe.runVirtualRule('select-name', { + it('should pass for role=none when disabled', () => { + const results = axe.runVirtualRule('select-name', { nodeName: 'select', attributes: { role: 'none', @@ -103,8 +148,8 @@ describe('select-name virtual-rule', function () { assert.lengthOf(results.incomplete, 0); }); - it('should incomplete for both missing aria-label and implicit label', function () { - var results = axe.runVirtualRule('select-name', { + it('should incomplete for both missing aria-label and implicit label', () => { + const results = axe.runVirtualRule('select-name', { nodeName: 'select', attributes: { 'aria-label': '' @@ -116,8 +161,8 @@ describe('select-name virtual-rule', function () { assert.lengthOf(results.incomplete, 1); }); - it('should fail when aria-label contains only whitespace and no implicit label', function () { - var node = new axe.SerialVirtualNode({ + it('should fail when aria-label contains only whitespace and no implicit label', () => { + const node = new axe.SerialVirtualNode({ nodeName: 'select', attributes: { 'aria-label': ' \t \n ' @@ -125,15 +170,15 @@ describe('select-name virtual-rule', function () { }); node.parent = null; - var results = axe.runVirtualRule('select-name', node); + const results = axe.runVirtualRule('select-name', node); assert.lengthOf(results.passes, 0); assert.lengthOf(results.violations, 1); assert.lengthOf(results.incomplete, 0); }); - it('should fail when aria-label is empty and no implicit label', function () { - var node = new axe.SerialVirtualNode({ + it('should fail when aria-label is empty and no implicit label', () => { + const node = new axe.SerialVirtualNode({ nodeName: 'select', attributes: { 'aria-label': '' @@ -141,15 +186,88 @@ describe('select-name virtual-rule', function () { }); node.parent = null; - var results = axe.runVirtualRule('select-name', node); + const results = axe.runVirtualRule('select-name', node); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); + + it('should fail when aria-labelledby contains only whitespace and no implicit label in complete tree', () => { + const html = new axe.SerialVirtualNode({ + nodeName: 'html' + }); + const body = new axe.SerialVirtualNode({ + nodeName: 'body' + }); + const div = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + id: 'foobar' + }, + id: 'foobar' + }); + const text = new axe.SerialVirtualNode({ + nodeName: '#text', + nodeType: 3, + nodeValue: ' ' + }); + const select = new axe.SerialVirtualNode({ + nodeName: 'select', + attributes: { + 'aria-labelledby': 'foobar' + } + }); + html.parent = null; + html.children = [body]; + + body.parent = html; + body.children = [div, select]; + + div.parent = body; + div.children = [text]; + + text.parent = div; + + select.parent = body; + + const results = axe.runVirtualRule('select-name', select); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); + + it('should fail when aria-labelledby is missing and no implicit label in complete tree', () => { + const html = new axe.SerialVirtualNode({ + nodeName: 'html' + }); + const body = new axe.SerialVirtualNode({ + nodeName: 'body' + }); + const select = new axe.SerialVirtualNode({ + nodeName: 'select', + attributes: { + 'aria-labelledby': 'foobar' + } + }); + html.parent = null; + html.children = [body]; + + body.parent = html; + body.children = [select]; + + select.parent = body; + + const results = axe.runVirtualRule('select-name', select); assert.lengthOf(results.passes, 0); assert.lengthOf(results.violations, 1); assert.lengthOf(results.incomplete, 0); }); - it('should fail when title is empty and no implicit label', function () { - var node = new axe.SerialVirtualNode({ + it('should fail when title is empty and no implicit label', () => { + const node = new axe.SerialVirtualNode({ nodeName: 'select', attributes: { title: '' @@ -157,15 +275,15 @@ describe('select-name virtual-rule', function () { }); node.parent = null; - var results = axe.runVirtualRule('select-name', node); + const results = axe.runVirtualRule('select-name', node); assert.lengthOf(results.passes, 0); assert.lengthOf(results.violations, 1); assert.lengthOf(results.incomplete, 0); }); - it('should pass for role=presentation', function () { - var node = new axe.SerialVirtualNode({ + it('should pass for role=presentation', () => { + const node = new axe.SerialVirtualNode({ nodeName: 'select', attributes: { role: 'presentation' @@ -173,15 +291,15 @@ describe('select-name virtual-rule', function () { }); node.parent = null; - var results = axe.runVirtualRule('select-name', node); + const results = axe.runVirtualRule('select-name', node); assert.lengthOf(results.passes, 0); assert.lengthOf(results.violations, 1); assert.lengthOf(results.incomplete, 0); }); - it('should pass for role=none', function () { - var node = new axe.SerialVirtualNode({ + it('should pass for role=none', () => { + const node = new axe.SerialVirtualNode({ nodeName: 'select', attributes: { role: 'none' @@ -189,7 +307,7 @@ describe('select-name virtual-rule', function () { }); node.parent = null; - var results = axe.runVirtualRule('select-name', node); + const results = axe.runVirtualRule('select-name', node); assert.lengthOf(results.passes, 0); assert.lengthOf(results.violations, 1); From 89999c816cbf8695749591d058b93c1ff5c41c4a Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:18:19 -0600 Subject: [PATCH 2/6] changes --- lib/commons/dom/get-root-vnodes.js | 38 +++ lib/commons/dom/idrefs.js | 50 ++-- lib/commons/dom/index.js | 1 + lib/commons/text/accessible-text.js | 2 +- test/commons/dom/get-root-vnodes.js | 27 ++ test/commons/dom/idrefs.js | 261 +++++++++++++++--- test/integration/virtual-rules/button-name.js | 63 ++++- test/playground.html | 3 +- 8 files changed, 381 insertions(+), 64 deletions(-) create mode 100644 lib/commons/dom/get-root-vnodes.js create mode 100644 test/commons/dom/get-root-vnodes.js diff --git a/lib/commons/dom/get-root-vnodes.js b/lib/commons/dom/get-root-vnodes.js new file mode 100644 index 0000000000..e8c84ff703 --- /dev/null +++ b/lib/commons/dom/get-root-vnodes.js @@ -0,0 +1,38 @@ +import { nodeLookup } from '../../core/utils'; + +/** + * Return the document or document fragment (shadow DOM) + * @method getRootVNodes + * @memberof axe.commons.dom + * @instance + * @param {Element|VirtualNode} node + * @returns {VirtualNode[]} + */ +export default function getRootVNodes(node) { + const { vNode } = nodeLookup(node); + + if (vNode._rootNodes) { + return vNode._rootNodes; + } + + // top of tree + if (vNode.parent === null) { + return [vNode]; + } + + // disconnected tree + if (!vNode.parent) { + return undefined; + } + + // since the virtual tree does not have a #shadowRoot element the root virtual + // node is the shadow host element. however the shadow host element is not inside + // the shadow DOM tree so we return the children of the shadow host element in + // order to not cross shadow DOM boundaries + if (vNode.shadowId !== vNode.parent.shadowId) { + return [...vNode.parent.children]; + } + + vNode._rootNodes = getRootVNodes(vNode.parent); + return vNode._rootNodes; +} diff --git a/lib/commons/dom/idrefs.js b/lib/commons/dom/idrefs.js index c9688689c3..533b5019e6 100644 --- a/lib/commons/dom/idrefs.js +++ b/lib/commons/dom/idrefs.js @@ -1,9 +1,9 @@ -import getRootNode from './get-root-node'; +import getRootVNodes from './get-root-vnodes'; import { tokenList, nodeLookup, - closest, - querySelectorAll + querySelectorAll, + getRootNode } from '../../core/utils'; /** @@ -23,29 +23,41 @@ import { * */ function idrefs(node, attr) { - const { vNode, domNode } = nodeLookup(node); - const result = []; - let doc; - const attrValue = vNode?.attr(attr) ?? node.getAttribute(attr); + const { domNode, vNode } = nodeLookup(node); + const results = []; + const attrValue = vNode ? vNode.attr(attr) : node.getAttribute(attr); - if (attrValue) { + if (!attrValue) { + return results; + } + + try { + const root = getRootNode(domNode); for (const token of tokenList(attrValue)) { - try { - doc ??= getRootNode(domNode); - result.push(doc.getElementById(token)); - } catch { - // don't run QSA on detached nodes or partial trees - const root = closest(vNode, 'html'); - if (!root) { - throw new TypeError('Cannot resolve id references for non-DOM nodes'); - } + results.push(root.getElementById(token)); + } + } catch { + const rootVNodes = getRootVNodes(vNode); + if (!rootVNodes) { + throw new TypeError('Cannot resolve id references for non-DOM nodes'); + } - result.push(querySelectorAll(root, `#${token}`, vNode.shadowId)?.[0]); + for (const token of tokenList(attrValue)) { + let result = null; + + for (const root of rootVNodes) { + const foundNode = querySelectorAll(root, `#${token}`)[0]; + if (foundNode) { + result = foundNode; + break; + } } + + results.push(result); } } - return result; + return results; } export default idrefs; diff --git a/lib/commons/dom/index.js b/lib/commons/dom/index.js index 3449238aa1..fcb0b34115 100644 --- a/lib/commons/dom/index.js +++ b/lib/commons/dom/index.js @@ -15,6 +15,7 @@ export { default as getElementStack } from './get-element-stack'; export { default as getModalDialog } from './get-modal-dialog'; export { default as getOverflowHiddenAncestors } from './get-overflow-hidden-ancestors'; export { default as getRootNode } from './get-root-node'; +export { default as getRootVNodes } from './get-root-vnodes'; export { default as getScrollOffset } from './get-scroll-offset'; export { default as getTabbableElements } from './get-tabbable-elements'; export { default as getTargetRects } from './get-target-rects'; diff --git a/lib/commons/text/accessible-text.js b/lib/commons/text/accessible-text.js index 711d6d8beb..5cab563455 100644 --- a/lib/commons/text/accessible-text.js +++ b/lib/commons/text/accessible-text.js @@ -12,7 +12,7 @@ import { nodeLookup } from '../../core/utils'; * @return {string} */ function accessibleText(element, context) { - const { vNode } = nodeLookup(element); // throws an exception on purpose if axe._tree not correct + const { vNode } = nodeLookup(element); return accessibleTextVirtual(vNode, context); } diff --git a/test/commons/dom/get-root-vnodes.js b/test/commons/dom/get-root-vnodes.js new file mode 100644 index 0000000000..fdc32d35f9 --- /dev/null +++ b/test/commons/dom/get-root-vnodes.js @@ -0,0 +1,27 @@ +describe('dom.getRootVNodes', () => { + const getRootVNodes = axe.commons.dom.getRootVNodes; + const fixture = document.querySelector('#fixture'); + const queryShadowFixture = axe.testUtils.queryShadowFixture; + + it('should return the root vNode of complete tree', () => { + axe.setup(); + const expected = [axe.utils.getNodeFromTree(document.documentElement)]; + assert.deepEqual(getRootVNodes(fixture), expected); + }); + + it('should return undefined for disconnected tree', () => { + axe.setup(); + axe.utils.getNodeFromTree(document.documentElement).parent = undefined; + assert.isUndefined(getRootVNodes(fixture)); + }); + + it('should return each child of a shadow DOM host', () => { + const target = queryShadowFixture( + '
', + '
Hello World
' + ); + + const expected = target.parent.children; + assert.deepEqual(getRootVNodes(target), expected); + }); +}); diff --git a/test/commons/dom/idrefs.js b/test/commons/dom/idrefs.js index 8f5abc6d0c..d28cbd3542 100644 --- a/test/commons/dom/idrefs.js +++ b/test/commons/dom/idrefs.js @@ -1,12 +1,10 @@ function createContentIDR() { - 'use strict'; const group = document.createElement('div'); group.id = 'target'; return group; } function makeShadowTreeIDR(node) { - 'use strict'; const root = node.attachShadow({ mode: 'open' }); const div = document.createElement('div'); div.className = 'parent'; @@ -16,27 +14,22 @@ function makeShadowTreeIDR(node) { } describe('dom.idrefs', () => { - 'use strict'; - const fixture = document.getElementById('fixture'); const shadowSupported = axe.testUtils.shadowSupport.v1; + const idrefs = axe.commons.dom.idrefs; it('should find referenced nodes by ID', () => { fixture.innerHTML = '
' + '
'; - const start = document.getElementById('start'); - const expected = [ - document.getElementById('target1'), - document.getElementById('target2') - ]; - - assert.deepEqual( - axe.commons.dom.idrefs(start, 'aria-cats'), - expected, - 'Should find it!' - ); + const start = document.getElementById('start'), + expected = [ + document.getElementById('target1'), + document.getElementById('target2') + ]; + + assert.deepEqual(idrefs(start, 'aria-cats'), expected, 'Should find it!'); }); (shadowSupported ? it : xit)( @@ -50,7 +43,7 @@ describe('dom.idrefs', () => { const expected = [fixture.firstChild.shadowRoot.getElementById('target')]; assert.deepEqual( - axe.commons.dom.idrefs(start, 'target'), + idrefs(start, 'target'), expected, 'should only find stuff in the shadow DOM' ); @@ -69,7 +62,7 @@ describe('dom.idrefs', () => { const expected = [document.getElementById('target')]; assert.deepEqual( - axe.commons.dom.idrefs(start, 'target'), + idrefs(start, 'target'), expected, 'should only find stuff in the document' ); @@ -81,18 +74,14 @@ describe('dom.idrefs', () => { '
' + '
'; - const start = document.getElementById('start'); - const expected = [ - document.getElementById('target1'), - document.getElementById('target2'), - null - ]; - - assert.deepEqual( - axe.commons.dom.idrefs(start, 'aria-cats'), - expected, - 'Should find it!' - ); + const start = document.getElementById('start'), + expected = [ + document.getElementById('target1'), + document.getElementById('target2'), + null + ]; + + assert.deepEqual(idrefs(start, 'aria-cats'), expected, 'Should find it!'); }); it('should not fail when extra whitespace is used', () => { @@ -100,19 +89,207 @@ describe('dom.idrefs', () => { '
' + '
'; - const start = document.getElementById('start'); - const expected = [ - document.getElementById('target1'), - document.getElementById('target2'), - null - ]; - - assert.deepEqual( - axe.commons.dom.idrefs(start, 'aria-cats'), - expected, - 'Should find it!' - ); + const start = document.getElementById('start'), + expected = [ + document.getElementById('target1'), + document.getElementById('target2'), + null + ]; + + assert.deepEqual(idrefs(start, 'aria-cats'), expected, 'Should find it!'); }); - // virtual-node tests test throwing for non-DOM nodes and working with complete trees + describe('SerialVirtualNode', () => { + it('should find referenced nodes by ID', () => { + const root = new axe.SerialVirtualNode({ + nodeName: 'div' + }); + const start = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + 'aria-cats': 'target1 target2' + } + }); + const target1 = new axe.SerialVirtualNode({ + nodeName: 'div', + id: 'target1' + }); + const target2 = new axe.SerialVirtualNode({ + nodeName: 'div', + id: 'target2' + }); + + root.parent = null; + root.children = [start, target1, target2]; + + start.parent = root; + target1.parent = root; + target2.parent = root; + + assert.deepEqual( + idrefs(start, 'aria-cats'), + [target1, target2], + 'Should find it!' + ); + }); + + it('should find only referenced nodes within the current root: shadow DOM', () => { + const root = new axe.SerialVirtualNode({ + nodeName: 'div' + }); + const outsideTarget = new axe.SerialVirtualNode({ + nodeName: 'div', + id: 'target' + }); + const host = new axe.SerialVirtualNode({ + nodeName: 'div' + }); + const shadowParent = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + target: 'target' + } + }); + const shadowTarget = new axe.SerialVirtualNode({ + nodeName: 'div', + id: 'target' + }); + + root.parent = null; + root.children = [outsideTarget, host]; + + outsideTarget.parent = root; + + host.parent = root; + host.children = [shadowParent, shadowTarget]; + + shadowParent.parent = host; + shadowParent.shadowId = 'abc123'; + + shadowTarget.parent = host; + shadowTarget.shadowId = 'abc123'; + + assert.deepEqual( + idrefs(shadowParent, 'target'), + [shadowTarget], + 'should only find stuff in the shadow DOM' + ); + }); + + it('should find only referenced nodes within the current root: document', () => { + const root = new axe.SerialVirtualNode({ + nodeName: 'div' + }); + const outsideTarget = new axe.SerialVirtualNode({ + nodeName: 'div', + id: 'target' + }); + const start = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + target: 'target' + } + }); + const host = new axe.SerialVirtualNode({ + nodeName: 'div' + }); + const shadowParent = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + target: 'target' + } + }); + const shadowTarget = new axe.SerialVirtualNode({ + nodeName: 'div', + id: 'target' + }); + + root.parent = null; + root.children = [outsideTarget, start, host]; + + outsideTarget.parent = root; + start.parent = root; + + host.parent = root; + host.children = [shadowParent, shadowTarget]; + + shadowParent.parent = host; + shadowParent.shadowId = 'abc123'; + + shadowTarget.parent = host; + shadowTarget.shadowId = 'abc123'; + + assert.deepEqual( + idrefs(start, 'target'), + [outsideTarget], + 'should only find stuff in the document' + ); + }); + + it('should insert null if a reference is not found', () => { + const root = new axe.SerialVirtualNode({ + nodeName: 'div' + }); + const start = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + 'aria-cats': 'target1 target2 target3' + } + }); + const target1 = new axe.SerialVirtualNode({ + nodeName: 'div', + id: 'target1' + }); + const target2 = new axe.SerialVirtualNode({ + nodeName: 'div', + id: 'target2' + }); + + root.parent = null; + root.children = [start, target1, target2]; + + start.parent = root; + target1.parent = root; + target2.parent = root; + + assert.deepEqual( + idrefs(start, 'aria-cats'), + [target1, target2, null], + 'Should find it!' + ); + }); + + it('should not fail when extra whitespace is used', () => { + const root = new axe.SerialVirtualNode({ + nodeName: 'div' + }); + const start = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + 'aria-cats': ' \ttarget1 \n target2 target3 \n\t' + } + }); + const target1 = new axe.SerialVirtualNode({ + nodeName: 'div', + id: 'target1' + }); + const target2 = new axe.SerialVirtualNode({ + nodeName: 'div', + id: 'target2' + }); + + root.parent = null; + root.children = [start, target1, target2]; + + start.parent = root; + target1.parent = root; + target2.parent = root; + + assert.deepEqual( + idrefs(start, 'aria-cats'), + [target1, target2, null], + 'Should find it!' + ); + }); + }); }); diff --git a/test/integration/virtual-rules/button-name.js b/test/integration/virtual-rules/button-name.js index d54ef0f62d..2cbfc627db 100644 --- a/test/integration/virtual-rules/button-name.js +++ b/test/integration/virtual-rules/button-name.js @@ -12,7 +12,44 @@ describe('button-name virtual-rule', () => { assert.lengthOf(results.incomplete, 0); }); - it('should incomplete for aria-labelledby', () => { + it('should pass for aria-labelledby in complete tree', () => { + const root = new axe.SerialVirtualNode({ + nodeName: 'body' + }); + const node = new axe.SerialVirtualNode({ + nodeName: 'button', + attributes: { + 'aria-labelledby': 'foobar' + } + }); + const foobar = new axe.SerialVirtualNode({ + nodeName: 'div', + id: 'foobar' + }); + const text = new axe.SerialVirtualNode({ + nodeName: '#text', + nodeType: 3, + nodeValue: 'foobar' + }); + + root.parent = null; + root.children = [node, foobar]; + + node.parent = root; + + foobar.parent = root; + foobar.children = [text]; + + text.parent = foobar; + + const results = axe.runVirtualRule('button-name', node); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should incomplete for aria-labelledby in disconnected tree', () => { const node = new axe.SerialVirtualNode({ nodeName: 'button', attributes: { @@ -179,6 +216,30 @@ describe('button-name virtual-rule', () => { assert.lengthOf(results.incomplete, 0); }); + it('should fail for missing aria-labelledby in complete tree', () => { + const root = new axe.SerialVirtualNode({ + nodeName: 'body' + }); + const node = new axe.SerialVirtualNode({ + nodeName: 'button', + attributes: { + 'aria-labelledby': 'foobar' + } + }); + + root.parent = null; + root.children = [node]; + + node.parent = root; + node.children = []; + + const results = axe.runVirtualRule('button-name', node); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); + it('should fail when title is empty', () => { const node = new axe.SerialVirtualNode({ nodeName: 'button', diff --git a/test/playground.html b/test/playground.html index 29b6d3837d..f435e1e8cc 100644 --- a/test/playground.html +++ b/test/playground.html @@ -4,7 +4,8 @@
-

Hello World

+

Hello World

+
foo
From 3b23b65bf7dfb7dea348649482c4d807be670c59 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:20:17 -0600 Subject: [PATCH 3/6] test --- test/commons/dom/idrefs.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/commons/dom/idrefs.js b/test/commons/dom/idrefs.js index d28cbd3542..cbb4d63a10 100644 --- a/test/commons/dom/idrefs.js +++ b/test/commons/dom/idrefs.js @@ -291,5 +291,36 @@ describe('dom.idrefs', () => { 'Should find it!' ); }); + + it('should throw if in disconnected tree', () => { + const root = new axe.SerialVirtualNode({ + nodeName: 'div' + }); + const start = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + 'aria-cats': 'target1 target2' + } + }); + const target1 = new axe.SerialVirtualNode({ + nodeName: 'div', + id: 'target1' + }); + const target2 = new axe.SerialVirtualNode({ + nodeName: 'div', + id: 'target2' + }); + + root.parent = undefined; + root.children = [start, target1, target2]; + + start.parent = root; + target1.parent = root; + target2.parent = root; + + assert.throws(() => { + idrefs(start, 'aria-cats'); + }); + }); }); }); From 7bb026a9009f1543836e1d30ae6549b375202ee1 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:21:43 -0600 Subject: [PATCH 4/6] revert --- test/integration/virtual-rules/select-name.js | 190 ++++-------------- test/playground.html | 3 +- 2 files changed, 37 insertions(+), 156 deletions(-) diff --git a/test/integration/virtual-rules/select-name.js b/test/integration/virtual-rules/select-name.js index 8e2f71c59c..97783b389e 100644 --- a/test/integration/virtual-rules/select-name.js +++ b/test/integration/virtual-rules/select-name.js @@ -1,6 +1,6 @@ -describe('select-name virtual-rule', () => { - it('should pass for aria-label', () => { - const results = axe.runVirtualRule('select-name', { +describe('select-name virtual-rule', function () { + it('should pass for aria-label', function () { + var results = axe.runVirtualRule('select-name', { nodeName: 'select', attributes: { 'aria-label': 'foobar' @@ -12,8 +12,8 @@ describe('select-name virtual-rule', () => { assert.lengthOf(results.incomplete, 0); }); - it('should incomplete for aria-labelledby', () => { - const results = axe.runVirtualRule('select-name', { + it('should incomplete for aria-labelledby', function () { + var results = axe.runVirtualRule('select-name', { nodeName: 'select', attributes: { 'aria-labelledby': 'foobar' @@ -25,59 +25,14 @@ describe('select-name virtual-rule', () => { assert.lengthOf(results.incomplete, 1); }); - it('should pass for aria-labelledby in complete tree', () => { - const html = new axe.SerialVirtualNode({ - nodeName: 'html' - }); - const body = new axe.SerialVirtualNode({ - nodeName: 'body' - }); - const div = new axe.SerialVirtualNode({ - nodeName: 'div', - attributes: { - id: 'foobar' - }, - id: 'foobar' - }); - const text = new axe.SerialVirtualNode({ - nodeName: '#text', - nodeType: 3, - nodeValue: 'foobar' - }); - const select = new axe.SerialVirtualNode({ - nodeName: 'select', - attributes: { - 'aria-labelledby': 'foobar' - } - }); - html.parent = null; - html.children = [body]; - - body.parent = html; - body.children = [div, select]; - - div.parent = body; - div.children = [text]; - - text.parent = div; - - select.parent = body; - - const results = axe.runVirtualRule('select-name', select); - - assert.lengthOf(results.passes, 1); - assert.lengthOf(results.violations, 0); - assert.lengthOf(results.incomplete, 0); - }); - - it('should pass for implicit label', () => { - const node = new axe.SerialVirtualNode({ + it('should pass for implicit label', function () { + var node = new axe.SerialVirtualNode({ nodeName: 'select' }); - const parent = new axe.SerialVirtualNode({ + var parent = new axe.SerialVirtualNode({ nodeName: 'label' }); - const child = new axe.SerialVirtualNode({ + var child = new axe.SerialVirtualNode({ nodeName: '#text', nodeType: 3, nodeValue: 'foobar' @@ -85,30 +40,30 @@ describe('select-name virtual-rule', () => { node.parent = parent; parent.children = [child, node]; - const results = axe.runVirtualRule('select-name', node); + var results = axe.runVirtualRule('select-name', node); assert.lengthOf(results.passes, 1); assert.lengthOf(results.violations, 0); assert.lengthOf(results.incomplete, 0); }); - it('should incomplete for explicit label', () => { - const node = new axe.SerialVirtualNode({ + it('should incomplete for explicit label', function () { + var node = new axe.SerialVirtualNode({ nodeName: 'select', attributes: { id: 'foobar' } }); - const results = axe.runVirtualRule('select-name', node); + var results = axe.runVirtualRule('select-name', node); assert.lengthOf(results.passes, 0); assert.lengthOf(results.violations, 0); assert.lengthOf(results.incomplete, 1); }); - it('should pass for title', () => { - const results = axe.runVirtualRule('select-name', { + it('should pass for title', function () { + var results = axe.runVirtualRule('select-name', { nodeName: 'select', attributes: { title: 'foobar' @@ -120,8 +75,8 @@ describe('select-name virtual-rule', () => { assert.lengthOf(results.incomplete, 0); }); - it('should pass for role=presentation when disabled', () => { - const results = axe.runVirtualRule('select-name', { + it('should pass for role=presentation when disabled', function () { + var results = axe.runVirtualRule('select-name', { nodeName: 'select', attributes: { role: 'presentation', @@ -134,8 +89,8 @@ describe('select-name virtual-rule', () => { assert.lengthOf(results.incomplete, 0); }); - it('should pass for role=none when disabled', () => { - const results = axe.runVirtualRule('select-name', { + it('should pass for role=none when disabled', function () { + var results = axe.runVirtualRule('select-name', { nodeName: 'select', attributes: { role: 'none', @@ -148,8 +103,8 @@ describe('select-name virtual-rule', () => { assert.lengthOf(results.incomplete, 0); }); - it('should incomplete for both missing aria-label and implicit label', () => { - const results = axe.runVirtualRule('select-name', { + it('should incomplete for both missing aria-label and implicit label', function () { + var results = axe.runVirtualRule('select-name', { nodeName: 'select', attributes: { 'aria-label': '' @@ -161,8 +116,8 @@ describe('select-name virtual-rule', () => { assert.lengthOf(results.incomplete, 1); }); - it('should fail when aria-label contains only whitespace and no implicit label', () => { - const node = new axe.SerialVirtualNode({ + it('should fail when aria-label contains only whitespace and no implicit label', function () { + var node = new axe.SerialVirtualNode({ nodeName: 'select', attributes: { 'aria-label': ' \t \n ' @@ -170,15 +125,15 @@ describe('select-name virtual-rule', () => { }); node.parent = null; - const results = axe.runVirtualRule('select-name', node); + var results = axe.runVirtualRule('select-name', node); assert.lengthOf(results.passes, 0); assert.lengthOf(results.violations, 1); assert.lengthOf(results.incomplete, 0); }); - it('should fail when aria-label is empty and no implicit label', () => { - const node = new axe.SerialVirtualNode({ + it('should fail when aria-label is empty and no implicit label', function () { + var node = new axe.SerialVirtualNode({ nodeName: 'select', attributes: { 'aria-label': '' @@ -186,88 +141,15 @@ describe('select-name virtual-rule', () => { }); node.parent = null; - const results = axe.runVirtualRule('select-name', node); - - assert.lengthOf(results.passes, 0); - assert.lengthOf(results.violations, 1); - assert.lengthOf(results.incomplete, 0); - }); - - it('should fail when aria-labelledby contains only whitespace and no implicit label in complete tree', () => { - const html = new axe.SerialVirtualNode({ - nodeName: 'html' - }); - const body = new axe.SerialVirtualNode({ - nodeName: 'body' - }); - const div = new axe.SerialVirtualNode({ - nodeName: 'div', - attributes: { - id: 'foobar' - }, - id: 'foobar' - }); - const text = new axe.SerialVirtualNode({ - nodeName: '#text', - nodeType: 3, - nodeValue: ' ' - }); - const select = new axe.SerialVirtualNode({ - nodeName: 'select', - attributes: { - 'aria-labelledby': 'foobar' - } - }); - html.parent = null; - html.children = [body]; - - body.parent = html; - body.children = [div, select]; - - div.parent = body; - div.children = [text]; - - text.parent = div; - - select.parent = body; - - const results = axe.runVirtualRule('select-name', select); - - assert.lengthOf(results.passes, 0); - assert.lengthOf(results.violations, 1); - assert.lengthOf(results.incomplete, 0); - }); - - it('should fail when aria-labelledby is missing and no implicit label in complete tree', () => { - const html = new axe.SerialVirtualNode({ - nodeName: 'html' - }); - const body = new axe.SerialVirtualNode({ - nodeName: 'body' - }); - const select = new axe.SerialVirtualNode({ - nodeName: 'select', - attributes: { - 'aria-labelledby': 'foobar' - } - }); - html.parent = null; - html.children = [body]; - - body.parent = html; - body.children = [select]; - - select.parent = body; - - const results = axe.runVirtualRule('select-name', select); + var results = axe.runVirtualRule('select-name', node); assert.lengthOf(results.passes, 0); assert.lengthOf(results.violations, 1); assert.lengthOf(results.incomplete, 0); }); - it('should fail when title is empty and no implicit label', () => { - const node = new axe.SerialVirtualNode({ + it('should fail when title is empty and no implicit label', function () { + var node = new axe.SerialVirtualNode({ nodeName: 'select', attributes: { title: '' @@ -275,15 +157,15 @@ describe('select-name virtual-rule', () => { }); node.parent = null; - const results = axe.runVirtualRule('select-name', node); + var results = axe.runVirtualRule('select-name', node); assert.lengthOf(results.passes, 0); assert.lengthOf(results.violations, 1); assert.lengthOf(results.incomplete, 0); }); - it('should pass for role=presentation', () => { - const node = new axe.SerialVirtualNode({ + it('should pass for role=presentation', function () { + var node = new axe.SerialVirtualNode({ nodeName: 'select', attributes: { role: 'presentation' @@ -291,15 +173,15 @@ describe('select-name virtual-rule', () => { }); node.parent = null; - const results = axe.runVirtualRule('select-name', node); + var results = axe.runVirtualRule('select-name', node); assert.lengthOf(results.passes, 0); assert.lengthOf(results.violations, 1); assert.lengthOf(results.incomplete, 0); }); - it('should pass for role=none', () => { - const node = new axe.SerialVirtualNode({ + it('should pass for role=none', function () { + var node = new axe.SerialVirtualNode({ nodeName: 'select', attributes: { role: 'none' @@ -307,7 +189,7 @@ describe('select-name virtual-rule', () => { }); node.parent = null; - const results = axe.runVirtualRule('select-name', node); + var results = axe.runVirtualRule('select-name', node); assert.lengthOf(results.passes, 0); assert.lengthOf(results.violations, 1); diff --git a/test/playground.html b/test/playground.html index f435e1e8cc..29b6d3837d 100644 --- a/test/playground.html +++ b/test/playground.html @@ -4,8 +4,7 @@
-

Hello World

-
foo
+

Hello World

From f2f3e3e3e9d9adaec4823e9b0b8d97435aae7546 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:24:36 -0600 Subject: [PATCH 5/6] comment --- lib/commons/dom/get-root-vnodes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commons/dom/get-root-vnodes.js b/lib/commons/dom/get-root-vnodes.js index e8c84ff703..7bc3d948da 100644 --- a/lib/commons/dom/get-root-vnodes.js +++ b/lib/commons/dom/get-root-vnodes.js @@ -1,7 +1,7 @@ import { nodeLookup } from '../../core/utils'; /** - * Return the document or document fragment (shadow DOM) + * Return the vNode(s) * @method getRootVNodes * @memberof axe.commons.dom * @instance From 992afecd2049a8e4a9e6b9977ae689a9b3e9a803 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Mon, 25 Aug 2025 13:57:30 -0600 Subject: [PATCH 6/6] change function name, use cache --- lib/commons/dom/get-root-children.js | 48 +++++++++++++++++++++++++++ lib/commons/dom/get-root-vnodes.js | 38 --------------------- lib/commons/dom/idrefs.js | 4 +-- lib/commons/dom/index.js | 2 +- test/commons/dom/get-root-children.js | 30 +++++++++++++++++ test/commons/dom/get-root-vnodes.js | 27 --------------- test/commons/dom/idrefs.js | 10 +++--- 7 files changed, 85 insertions(+), 74 deletions(-) create mode 100644 lib/commons/dom/get-root-children.js delete mode 100644 lib/commons/dom/get-root-vnodes.js create mode 100644 test/commons/dom/get-root-children.js delete mode 100644 test/commons/dom/get-root-vnodes.js diff --git a/lib/commons/dom/get-root-children.js b/lib/commons/dom/get-root-children.js new file mode 100644 index 0000000000..47a03daeac --- /dev/null +++ b/lib/commons/dom/get-root-children.js @@ -0,0 +1,48 @@ +import { nodeLookup } from '../../core/utils'; +import cache from '../../core/base/cache'; + +/** + * Return the child virtual nodes of the root node + * @method getRootChildren + * @memberof axe.commons.dom + * @instance + * @param {Element|VirtualNode} node + * @returns {VirtualNode[]|undefined} + */ +export default function getRootChildren(node) { + const { vNode } = nodeLookup(node); + const { shadowId } = vNode; + + const childrenMap = cache.get('getRootChildrenMap', () => ({})); + if (childrenMap[shadowId]) { + return childrenMap[shadowId]; + } + + // top of tree + if (vNode.parent === null) { + childrenMap[shadowId] = [...vNode.children]; + return childrenMap[shadowId]; + } + + // disconnected tree + if (!vNode.parent) { + childrenMap[shadowId] = undefined; + return childrenMap[shadowId]; + } + + // since the virtual tree does not have a #shadowRoot element the root virtual + // node is the shadow host element. however the shadow host element is not inside + // the shadow DOM tree so we return the children of the shadow host element in + // order to not cross shadow DOM boundaries. + // + // TODO: slotted elements share the shadowId of the shadow tree it is attached to + // but should not be used to find id's inside the shadow tree. throw an error + // until we resolve this + if (vNode.shadowId !== vNode.parent.shadowId) { + throw new Error( + 'Getting root children of shadow DOM elements is not supported' + ); + } + + return getRootChildren(vNode.parent); +} diff --git a/lib/commons/dom/get-root-vnodes.js b/lib/commons/dom/get-root-vnodes.js deleted file mode 100644 index 7bc3d948da..0000000000 --- a/lib/commons/dom/get-root-vnodes.js +++ /dev/null @@ -1,38 +0,0 @@ -import { nodeLookup } from '../../core/utils'; - -/** - * Return the vNode(s) - * @method getRootVNodes - * @memberof axe.commons.dom - * @instance - * @param {Element|VirtualNode} node - * @returns {VirtualNode[]} - */ -export default function getRootVNodes(node) { - const { vNode } = nodeLookup(node); - - if (vNode._rootNodes) { - return vNode._rootNodes; - } - - // top of tree - if (vNode.parent === null) { - return [vNode]; - } - - // disconnected tree - if (!vNode.parent) { - return undefined; - } - - // since the virtual tree does not have a #shadowRoot element the root virtual - // node is the shadow host element. however the shadow host element is not inside - // the shadow DOM tree so we return the children of the shadow host element in - // order to not cross shadow DOM boundaries - if (vNode.shadowId !== vNode.parent.shadowId) { - return [...vNode.parent.children]; - } - - vNode._rootNodes = getRootVNodes(vNode.parent); - return vNode._rootNodes; -} diff --git a/lib/commons/dom/idrefs.js b/lib/commons/dom/idrefs.js index 533b5019e6..bb55ef9da1 100644 --- a/lib/commons/dom/idrefs.js +++ b/lib/commons/dom/idrefs.js @@ -1,4 +1,4 @@ -import getRootVNodes from './get-root-vnodes'; +import getRootChildren from './get-root-children'; import { tokenList, nodeLookup, @@ -37,7 +37,7 @@ function idrefs(node, attr) { results.push(root.getElementById(token)); } } catch { - const rootVNodes = getRootVNodes(vNode); + const rootVNodes = getRootChildren(vNode); if (!rootVNodes) { throw new TypeError('Cannot resolve id references for non-DOM nodes'); } diff --git a/lib/commons/dom/index.js b/lib/commons/dom/index.js index fcb0b34115..f9c40eebab 100644 --- a/lib/commons/dom/index.js +++ b/lib/commons/dom/index.js @@ -14,8 +14,8 @@ export { default as getElementCoordinates } from './get-element-coordinates'; export { default as getElementStack } from './get-element-stack'; export { default as getModalDialog } from './get-modal-dialog'; export { default as getOverflowHiddenAncestors } from './get-overflow-hidden-ancestors'; +export { default as getRootChildren } from './get-root-children'; export { default as getRootNode } from './get-root-node'; -export { default as getRootVNodes } from './get-root-vnodes'; export { default as getScrollOffset } from './get-scroll-offset'; export { default as getTabbableElements } from './get-tabbable-elements'; export { default as getTargetRects } from './get-target-rects'; diff --git a/test/commons/dom/get-root-children.js b/test/commons/dom/get-root-children.js new file mode 100644 index 0000000000..d203d06f08 --- /dev/null +++ b/test/commons/dom/get-root-children.js @@ -0,0 +1,30 @@ +describe('dom.getRootChildren', () => { + const getRootChildren = axe.commons.dom.getRootChildren; + const fixture = document.querySelector('#fixture'); + const queryShadowFixture = axe.testUtils.queryShadowFixture; + + it('should return the children of the root node of a complete tree', () => { + axe.setup(); + const expected = axe.utils.getNodeFromTree( + document.documentElement + ).children; + assert.deepEqual(getRootChildren(fixture), expected); + }); + + it('should return undefined for disconnected tree', () => { + axe.setup(); + axe.utils.getNodeFromTree(document.documentElement).parent = undefined; + assert.isUndefined(getRootChildren(fixture)); + }); + + it('should throw for shadow DOM', () => { + const target = queryShadowFixture( + '
', + '
Hello World
' + ); + + assert.throws(() => { + getRootChildren(target); + }); + }); +}); diff --git a/test/commons/dom/get-root-vnodes.js b/test/commons/dom/get-root-vnodes.js deleted file mode 100644 index fdc32d35f9..0000000000 --- a/test/commons/dom/get-root-vnodes.js +++ /dev/null @@ -1,27 +0,0 @@ -describe('dom.getRootVNodes', () => { - const getRootVNodes = axe.commons.dom.getRootVNodes; - const fixture = document.querySelector('#fixture'); - const queryShadowFixture = axe.testUtils.queryShadowFixture; - - it('should return the root vNode of complete tree', () => { - axe.setup(); - const expected = [axe.utils.getNodeFromTree(document.documentElement)]; - assert.deepEqual(getRootVNodes(fixture), expected); - }); - - it('should return undefined for disconnected tree', () => { - axe.setup(); - axe.utils.getNodeFromTree(document.documentElement).parent = undefined; - assert.isUndefined(getRootVNodes(fixture)); - }); - - it('should return each child of a shadow DOM host', () => { - const target = queryShadowFixture( - '
', - '
Hello World
' - ); - - const expected = target.parent.children; - assert.deepEqual(getRootVNodes(target), expected); - }); -}); diff --git a/test/commons/dom/idrefs.js b/test/commons/dom/idrefs.js index cbb4d63a10..eba811682c 100644 --- a/test/commons/dom/idrefs.js +++ b/test/commons/dom/idrefs.js @@ -133,7 +133,7 @@ describe('dom.idrefs', () => { ); }); - it('should find only referenced nodes within the current root: shadow DOM', () => { + it('should throw for elements in shadow DOM', () => { const root = new axe.SerialVirtualNode({ nodeName: 'div' }); @@ -169,11 +169,9 @@ describe('dom.idrefs', () => { shadowTarget.parent = host; shadowTarget.shadowId = 'abc123'; - assert.deepEqual( - idrefs(shadowParent, 'target'), - [shadowTarget], - 'should only find stuff in the shadow DOM' - ); + assert.throws(() => { + idrefs(shadowParent, 'target'); + }); }); it('should find only referenced nodes within the current root: document', () => {