Skip to content

Commit

Permalink
fix(utils): unify selecting nodes in shadow tree with shadowSelect() (#…
Browse files Browse the repository at this point in the history
…3068)

* fix(utils): unify use of getFrameContexts

Adds axe.utils.shadowSelect to find frames from getFrameContext()

* 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 Jul 9, 2021
1 parent 73d3ae1 commit 21681da
Show file tree
Hide file tree
Showing 3 changed files with 119 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 filterHtmlAttrs } from './filter-html-attrs';
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 toArray } from './to-array';
export { default as tokenList } from './token-list';
export { default as uniqueArray } from './unique-array';
Expand Down
26 changes: 26 additions & 0 deletions lib/core/utils/shadow-select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Find the first element 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 {Element|Null}
*/
export default function shadowSelect(selectors) {
// Spread to avoid mutating the input
const selectorArr = Array.isArray(selectors) ? [...selectors] : [selectors];
return selectRecursive(selectorArr, document)
}

/* Find an element in shadow or light DOM trees, using an axe selector */
function selectRecursive(selectors, doc) {
const selectorStr = selectors.shift();
const elm = selectorStr ? doc.querySelector(selectorStr) : null;
if (selectors.length === 0) {
return elm;
}
if (!elm?.shadowRoot) {
return null;
}
return selectRecursive(selectors, elm.shadowRoot);
}
92 changes: 92 additions & 0 deletions test/core/utils/shadow-select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
var shadowSupported = axe.testUtils.shadowSupport.v1;
var testSuite = (shadowSupported ? describe : describe.skip)

testSuite('utils.shadowSelect', function () {
var shadowSelect = axe.utils.shadowSelect;
var fixture = document.querySelector('#fixture');

afterEach(function () {
fixture.innerHTML = '';
});

it('throws when not passed a string or array', function () {
assert.throws(function () {
shadowSelect(123);
});
});

it('throws when passed an array with non-string values', function () {
assert.throws(function () {
shadowSelect([123]);
});
});

describe('given a string', function () {
it('returns null if no node is found', function () {
fixture.innerHTML = '<b class="hello"></b>';
assert.isNull(shadowSelect('.goodbye'));
});

it('returns the first matching element in the document', function () {
fixture.innerHTML = '<b class="hello"></b><i class="hello"></i>';
var node = shadowSelect('.hello');
assert.equal(node.nodeName.toLowerCase(), 'b');
});
});

describe('given an array of string', function () {
function appendShadowTree(parentNode, nodeName) {
var node = document.createElement(nodeName);
parentNode.appendChild(node);
return node.attachShadow({ mode: 'open' });
}

it('returns null given an empty array', function () {
assert.isNull(shadowSelect([]));
});

it('returns null if the node does not exist in the shadow tree', function () {
var shadowRoot = appendShadowTree(fixture, 'div')
shadowRoot.innerHTML = '<b class="hello"></b>';
assert.isNull(shadowSelect(['#fixture > div', '.goodbye']));
});

it('returns null if an intermediate node is not a shadow root', function () {
var shadowRoot = appendShadowTree(fixture, 'article');
shadowRoot.innerHTML = '<section><p class="hello"></p></section>';
assert.isNull(shadowSelect(['#fixture > article', 'section', 'p']));
});

it('returns from Document with a length of 1', function () {
fixture.innerHTML = '<b class="hello"></b><i class="hello"></i>';
var node = shadowSelect(['.hello']);
assert.equal(node.nodeName.toLowerCase(), 'b');
});

it('returns from a shadow tree with length 2', function () {
var shadowRoot = appendShadowTree(fixture, 'div');
shadowRoot.innerHTML = '<b class="hello"></b><i class="hello"></i>';

var node = shadowSelect(['#fixture > div', '.hello']);
assert.equal(node.nodeName.toLowerCase(), 'b');
});

it('returns a node from multiple trees deep', function () {
var root = fixture;
var nodes = ['article', 'section', 'div', 'p'];
nodes.forEach(function (nodeName) {
root = appendShadowTree(root, nodeName);
});
root.innerHTML = '<b class="hello"></b><i class="hello"></i>';

var node = shadowSelect([
'#fixture > article',
'section',
'div',
'p',
'.hello'
]);
assert.equal(node.nodeName.toLowerCase(), 'b');
});
});
});

0 comments on commit 21681da

Please sign in to comment.