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', () => {