diff --git a/lib/commons/aria/get-accessible-refs.js b/lib/commons/aria/get-accessible-refs.js index 1f0ed55d57..9ad5627f03 100644 --- a/lib/commons/aria/get-accessible-refs.js +++ b/lib/commons/aria/get-accessible-refs.js @@ -13,22 +13,26 @@ function cacheIdRefs(node, idRefs, refAttrs) { if (node.hasAttribute) { if (node.nodeName.toUpperCase() === 'LABEL' && node.hasAttribute('for')) { const id = node.getAttribute('for'); - idRefs[id] = idRefs[id] || []; - idRefs[id].push(node); + if (!idRefs.has(id)) { + idRefs.set(id, [node]); + } else { + idRefs.get(id).push(node); + } } for (let i = 0; i < refAttrs.length; ++i) { const attr = refAttrs[i]; const attrValue = sanitize(node.getAttribute(attr) || ''); - if (!attrValue) { continue; } - const tokens = tokenList(attrValue); - for (let k = 0; k < tokens.length; ++k) { - idRefs[tokens[k]] = idRefs[tokens[k]] || []; - idRefs[tokens[k]].push(node); + for (const token of tokenList(attrValue)) { + if (!idRefs.has(token)) { + idRefs.set(token, [node]); + } else { + idRefs.get(token).push(node); + } } } } @@ -50,22 +54,21 @@ function getAccessibleRefs(node) { let root = getRootNode(node); root = root.documentElement || root; // account for shadow roots - const idRefsByRoot = cache.get('idRefsByRoot', () => new WeakMap()); + const idRefsByRoot = cache.get('idRefsByRoot', () => new Map()); let idRefs = idRefsByRoot.get(root); if (!idRefs) { - idRefs = {}; + idRefs = new Map(); idRefsByRoot.set(root, idRefs); const refAttrs = Object.keys(standards.ariaAttrs).filter(attr => { const { type } = standards.ariaAttrs[attr]; return idRefsRegex.test(type); }); - cacheIdRefs(root, idRefs, refAttrs); } - return idRefs[node.id] || []; + return idRefs.get(node.id) ?? []; } export default getAccessibleRefs; diff --git a/lib/core/utils/find-by.js b/lib/core/utils/find-by.js index ee84ecdb35..252a0c1903 100644 --- a/lib/core/utils/find-by.js +++ b/lib/core/utils/find-by.js @@ -9,7 +9,13 @@ */ function findBy(array, key, value) { if (Array.isArray(array)) { - return array.find(obj => typeof obj === 'object' && obj[key] === value); + return array.find( + obj => + obj !== null && + typeof obj === 'object' && + Object.hasOwn(obj, key) && + obj[key] === value + ); } } diff --git a/lib/core/utils/selector-cache.js b/lib/core/utils/selector-cache.js index 5f81700678..1771c89c76 100644 --- a/lib/core/utils/selector-cache.js +++ b/lib/core/utils/selector-cache.js @@ -91,9 +91,12 @@ function findMatchingNodes(expression, selectorMap, shadowId) { nodes = selectorMap['*']; } else { if (exp.id) { - // a selector must match all parts, otherwise we can just exit - // early - if (!selectorMap[idsKey] || !selectorMap[idsKey][exp.id]?.length) { + // a selector must match all parts, otherwise we can just exit early + if ( + !selectorMap[idsKey] || + !Object.hasOwn(selectorMap[idsKey], exp.id) || + !selectorMap[idsKey][exp.id]?.length + ) { return; } @@ -176,7 +179,9 @@ function getSharedValues(a, b) { * @param {Object} map */ function cacheSelector(key, vNode, map) { - map[key] = map[key] || []; + if (!Object.hasOwn(map, key)) { + map[key] = []; + } map[key].push(vNode); } diff --git a/lib/standards/html-elms.js b/lib/standards/html-elms.js index 2ccb779c7c..1b75a8bbb8 100644 --- a/lib/standards/html-elms.js +++ b/lib/standards/html-elms.js @@ -358,7 +358,7 @@ const htmlElms = { 'menuitem', 'menuitemcheckbox', 'menuitemradio', - 'meter', + 'meter', 'option', 'progressbar', 'radio', diff --git a/test/commons/aria/get-accessible-refs.js b/test/commons/aria/get-accessible-refs.js index 57941a5336..00820526e3 100644 --- a/test/commons/aria/get-accessible-refs.js +++ b/test/commons/aria/get-accessible-refs.js @@ -80,6 +80,34 @@ describe('aria.getAccessibleRefs', function () { assert.deepEqual(getAccessibleRefs(node), [ref]); }); + describe('when JavaScript object names are used as IDs', function () { + const ids = [ + 'prototype', + 'constructor', + '__proto__', + 'Element', + 'nodeName', + 'valueOf', + 'toString' + ]; + for (const id of ids) { + it(`does not break with id="${id}"`, function () { + setLookup({ 'aria-bar': { type: 'idrefs' } }); + fixture.innerHTML = `
`; + + var node = document.getElementById(id); + var ref = document.getElementById('ref'); + assert.deepEqual( + getAccessibleRefs(node), + [ref], + `Not equal for ID ${id}` + ); + }); + } + }); + (shadowSupport ? it : xit)('works inside shadow DOM', function () { setLookup({ 'aria-bar': { type: 'idref' } }); fixture.innerHTML = '
'; diff --git a/test/core/utils/find-by.js b/test/core/utils/find-by.js index 79911a6ff8..e6482fcec3 100644 --- a/test/core/utils/find-by.js +++ b/test/core/utils/find-by.js @@ -40,4 +40,21 @@ describe('axe.utils.findBy', function () { it('should not throw if passed falsey first parameter', function () { assert.isUndefined(axe.utils.findBy(null, 'id', 'macaque')); }); + + it('ignores any non-object elements in the array', function () { + const obj = { + id: 'monkeys', + foo: 'bar' + }; + const array = ['bananas', true, null, 123, obj]; + + assert.equal(axe.utils.findBy(array, 'id', 'monkeys'), obj); + }); + + it('only looks at owned properties', function () { + const obj1 = { id: 'monkeys', eat: 'bananas' }; + const obj2 = Object.create(obj1); + obj2.id = 'gorillas'; + assert.equal(axe.utils.findBy([obj2, obj1], 'eat', 'bananas'), obj1); + }); }); diff --git a/test/core/utils/selector-cache.js b/test/core/utils/selector-cache.js index 9e6e70deeb..7a06c5288e 100644 --- a/test/core/utils/selector-cache.js +++ b/test/core/utils/selector-cache.js @@ -55,6 +55,29 @@ describe('utils.selector-cache', function () { assert.lengthOf(Object.keys(map), 0); }); + + describe('with javascripty attribute selectors', function () { + const terms = [ + 'prototype', + 'constructor', + '__proto__', + 'Element', + 'nodeName', + 'valueOf', + 'toString' + ]; + for (const term of terms) { + it(`works with ${term}`, function () { + fixture.innerHTML = `
`; + const vNode = new axe.VirtualNode(fixture.firstChild); + const map = {}; + cacheNodeSelectors(vNode, map); + assert.deepEqual(map['[id]'], [vNode]); + assert.deepEqual(map['[class]'], [vNode]); + assert.deepEqual(map['[aria-label]'], [vNode]); + }); + } + }); }); describe('getNodesMatchingExpression', function () { diff --git a/test/integration/full/all-rules/all-rules.html b/test/integration/full/all-rules/all-rules.html index 05c32b4ad4..ec5a5af5c1 100644 --- a/test/integration/full/all-rules/all-rules.html +++ b/test/integration/full/all-rules/all-rules.html @@ -28,11 +28,11 @@ >
-
+
monkeys -
Foo
+
Foo
Home
@@ -51,7 +51,7 @@
Item
- +

Banana error

text

diff --git a/test/integration/rules/aria-allowed-role/aria-allowed-role.html b/test/integration/rules/aria-allowed-role/aria-allowed-role.html index 24e4f8ba48..46f11eb1b2 100644 --- a/test/integration/rules/aria-allowed-role/aria-allowed-role.html +++ b/test/integration/rules/aria-allowed-role/aria-allowed-role.html @@ -246,7 +246,15 @@

ok
ok
-test +test
hazaar