-
Notifications
You must be signed in to change notification settings - Fork 763
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(utils): unify selecting nodes in shadow tree with shadowSelect() (#…
…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
1 parent
73d3ae1
commit 21681da
Showing
3 changed files
with
119 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); | ||
}); |