diff --git a/lib/core/utils/index.js b/lib/core/utils/index.js index 6600f497b4..8f919d1546 100644 --- a/lib/core/utils/index.js +++ b/lib/core/utils/index.js @@ -77,6 +77,7 @@ export { default as select } from './select'; export { default as sendCommandToFrame } from './send-command-to-frame'; export { default as setScrollState } from './set-scroll-state'; export { default as shadowSelect } from './shadow-select'; +export { default as shadowSelectAll } from './shadow-select-all'; export { default as toArray } from './to-array'; export { default as tokenList } from './token-list'; export { default as uniqueArray } from './unique-array'; diff --git a/lib/core/utils/shadow-select-all.js b/lib/core/utils/shadow-select-all.js new file mode 100644 index 0000000000..2f311a40e6 --- /dev/null +++ b/lib/core/utils/shadow-select-all.js @@ -0,0 +1,31 @@ +/** + * Find elements to match a selector. + * Use an array of selectors to reach into shadow DOM trees + * + * @param {string|string[]} selector String or array of strings with a CSS selector + * @param {Document} doc Optional document node + * @returns {Node[]} + */ +export default function shadowSelectAll(selectors, doc = document) { + // Spread to avoid mutating the input + const selectorArr = Array.isArray(selectors) ? [...selectors] : [selectors]; + if (selectors.length === 0) { + return []; + } + return selectAllRecursive(selectorArr, doc); +} + +/* Find elements in shadow or light DOM trees, using an array of selectors */ +function selectAllRecursive([selectorStr, ...restSelector], doc) { + const elms = doc.querySelectorAll(selectorStr); + if (restSelector.length === 0) { + return Array.from(elms); + } + const selected = []; + for (const elm of elms) { + if (elm?.shadowRoot) { + selected.push(...selectAllRecursive(restSelector, elm.shadowRoot)); + } + } + return selected; +} diff --git a/test/core/utils/shadow-select-all.js b/test/core/utils/shadow-select-all.js new file mode 100644 index 0000000000..225f6685b0 --- /dev/null +++ b/test/core/utils/shadow-select-all.js @@ -0,0 +1,93 @@ +describe('utils.shadowSelectAll', () => { + const shadowSelectAll = axe.utils.shadowSelectAll; + const fixture = document.querySelector('#fixture'); + const mapNodeName = elms => elms.map(elm => elm.nodeName.toLowerCase()); + + it('throws when not passed a string or array', () => { + assert.throws(() => { + shadowSelectAll(123); + }); + }); + + it('throws when passed an array with non-string values', () => { + assert.throws(() => { + shadowSelectAll([123]); + }); + }); + + describe('given a string', () => { + it('returns [] if no node is found', () => { + fixture.innerHTML = ''; + assert.deepEqual(shadowSelectAll('.goodbye'), []); + }); + + it('returns the each matching element in the document', () => { + fixture.innerHTML = ` + + `; + const nodes = shadowSelectAll('#fixture > .hello'); + assert.deepEqual(mapNodeName(nodes), ['b', 'i']); + }); + }); + + describe('given an array of string', () => { + function addShadowTree(host, html) { + const root = host.attachShadow({ mode: 'open' }); + root.innerHTML = html; + return root; + } + + it('returns [] given an empty array', () => { + assert.deepEqual(shadowSelectAll([]), []); + }); + + it('returns [] if the shadow host does not exist', () => { + fixture.innerHTML = '
'; + addShadowTree(fixture.children[0], ``); + assert.deepEqual(shadowSelectAll(['#fixture > span', 'b']), []); + }); + + it('returns [] if the no final element exists', () => { + fixture.innerHTML = ''; + addShadowTree(fixture.children[0], ``); + assert.deepEqual(shadowSelectAll(['span', 'b']), []); + }); + + it('returns nodes from a shadow tree', () => { + fixture.innerHTML = ''; + addShadowTree(fixture.children[0], ``); + const nodeNames = mapNodeName(shadowSelectAll(['#fixture > span', '*'])); + assert.deepEqual(nodeNames, ['b', 'i']); + }); + + it('returns nodes from multiple shadow trees', () => { + fixture.innerHTML = ''; + addShadowTree(fixture.children[0], ``); + addShadowTree(fixture.children[1], ``); + const nodeNames = mapNodeName(shadowSelectAll(['#fixture > span', '*'])); + assert.deepEqual(nodeNames, ['a', 'b', 'i', 's']); + }); + + it('returns nodes from multiple trees deep', () => { + fixture.innerHTML = '
'; + const root1 = addShadowTree( + fixture.children[0], + '' + ); + const root2 = addShadowTree( + fixture.children[1], + '
' + ); + + addShadowTree(root1.children[0], ''); + addShadowTree(root1.children[1], ''); + addShadowTree(root2.children[0], ''); + addShadowTree(root2.children[1], ''); + + const nodeNames = mapNodeName( + shadowSelectAll(['#fixture > div', 'span', '*']) + ); + assert.deepEqual(nodeNames, ['a', 'b', 's']); + }); + }); +});