Skip to content

Commit

Permalink
feat(utils): new shadowSelectAll utility (#3796)
Browse files Browse the repository at this point in the history
* feat(utils): new shadowSelectAll utility

* Apply suggestions from code review

Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com>

Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com>
  • Loading branch information
WilcoFiers and straker committed Nov 23, 2022
1 parent 757a6fc commit 5865462
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/core/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
31 changes: 31 additions & 0 deletions lib/core/utils/shadow-select-all.js
Original file line number Diff line number Diff line change
@@ -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;
}
93 changes: 93 additions & 0 deletions test/core/utils/shadow-select-all.js
Original file line number Diff line number Diff line change
@@ -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 = '<b class="hello"></b>';
assert.deepEqual(shadowSelectAll('.goodbye'), []);
});

it('returns the each matching element in the document', () => {
fixture.innerHTML = `<b class="hello"></b>
<s class="goodbye"></s>
<i class="hello"></i>`;
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 = '<div></div>';
addShadowTree(fixture.children[0], `<b></b>`);
assert.deepEqual(shadowSelectAll(['#fixture > span', 'b']), []);
});

it('returns [] if the no final element exists', () => {
fixture.innerHTML = '<span></span>';
addShadowTree(fixture.children[0], `<i></i>`);
assert.deepEqual(shadowSelectAll(['span', 'b']), []);
});

it('returns nodes from a shadow tree', () => {
fixture.innerHTML = '<span></span>';
addShadowTree(fixture.children[0], `<b></b><i></i>`);
const nodeNames = mapNodeName(shadowSelectAll(['#fixture > span', '*']));
assert.deepEqual(nodeNames, ['b', 'i']);
});

it('returns nodes from multiple shadow trees', () => {
fixture.innerHTML = '<span></span><span></span>';
addShadowTree(fixture.children[0], `<a></a><b></b>`);
addShadowTree(fixture.children[1], `<i></i><s></s>`);
const nodeNames = mapNodeName(shadowSelectAll(['#fixture > span', '*']));
assert.deepEqual(nodeNames, ['a', 'b', 'i', 's']);
});

it('returns nodes from multiple trees deep', () => {
fixture.innerHTML = '<div></div><div></div>';
const root1 = addShadowTree(
fixture.children[0],
'<span></span><span></span>'
);
const root2 = addShadowTree(
fixture.children[1],
'<div></div><span></span>'
);

addShadowTree(root1.children[0], '<a></a>');
addShadowTree(root1.children[1], '<b></b>');
addShadowTree(root2.children[0], '<i></i>');
addShadowTree(root2.children[1], '<s></s>');

const nodeNames = mapNodeName(
shadowSelectAll(['#fixture > div', 'span', '*'])
);
assert.deepEqual(nodeNames, ['a', 'b', 's']);
});
});
});

0 comments on commit 5865462

Please sign in to comment.