Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(utils): new shadowSelectAll utility #3796

Merged
merged 2 commits into from
Nov 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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']);
});
});
});