Skip to content

Commit d4aee16

Browse files
pomerantsevstraker
andauthored
fix: allow shadow roots in axe.run contexts (#4952)
Before: if a shadow root (not the shadow host element, but an actual [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot) instance) is passed as part of context, it's silently ignored. After: the shadow root node is substituted with its ~host element~ children for the rest of Axe algorithms to work with. Closes: #4941 --------- Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com>
1 parent 1680105 commit d4aee16

File tree

4 files changed

+69
-3
lines changed

4 files changed

+69
-3
lines changed

lib/core/base/context/parse-selector-array.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ export function parseSelectorArray(context, type) {
1616
if (item instanceof window.Node) {
1717
if (item.documentElement instanceof window.Node) {
1818
result.push(context.flatTree[0]);
19+
} else if (item.host instanceof window.Node) {
20+
// Item is a shadow root. We only cache instances of `Element`,
21+
// not `DocumentFragment`, so instead of the shadow root itself,
22+
// we'll push all of its children to context.
23+
const children = Array.from(item.children).map(child =>
24+
getNodeFromTree(child)
25+
);
26+
result.push(...children);
1927
} else {
2028
result.push(getNodeFromTree(item));
2129
}

test/core/base/context.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ describe('Context', () => {
1616
it('should not mutate exclude in input', () => {
1717
fixture.innerHTML = '<div id="foo"></div>';
1818
const context = { exclude: [['iframe', '#foo']] };
19-
// eslint-disable-next-line no-new
19+
2020
new Context(context);
2121
assert.deepEqual(context, { exclude: [['iframe', '#foo']] });
2222
});
2323

2424
it('should not mutate its include input', () => {
2525
fixture.innerHTML = '<div id="foo"></div>';
2626
const context = { include: [['#foo']] };
27-
// eslint-disable-next-line no-new
27+
2828
new Context(context);
2929
assert.deepEqual(context, { include: [['#foo']] });
3030
});
@@ -109,6 +109,19 @@ describe('Context', () => {
109109
assert.equal(result.include[0].props.id, 'target');
110110
});
111111

112+
it('accepts a reference to a ShadowRoot', () => {
113+
createNestedShadowDom(
114+
fixture,
115+
'<article id="shadowHost"></article>',
116+
`<h1 id="h1">Heading</h1>
117+
<p id="p">Content</p>`
118+
);
119+
const shadowHost = fixture.querySelector('#shadowHost');
120+
const shadowRoot = shadowHost.shadowRoot;
121+
const result = new Context(shadowRoot);
122+
assert.deepEqual(selectors(result.include), ['#h1', '#p']);
123+
});
124+
112125
it('accepts a node reference consisting of nested divs', () => {
113126
const div1 = document.createElement('div');
114127
const div2 = document.createElement('div');

test/integration/full/context/context.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
</ul>
3030
<div id="shadow-container">
3131
<p id="test-passes">Passing Text</p>
32-
<div id="shadow-host"></div>
32+
<div id="shadow-host" aria-sort="both"></div>
3333
</div>
3434
<div id="mocha"></div>
3535
<script src="/test/testutils.js"></script>

test/integration/full/context/context.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,51 @@ describe('context test', function () {
362362
}
363363
);
364364
});
365+
366+
describe('Shadow DOM', function () {
367+
var sConfig = {
368+
runOnly: {
369+
type: 'rule',
370+
values: ['aria-allowed-attr', 'color-contrast']
371+
}
372+
};
373+
it('when passed a shadow host, reports issues both on itself and in the shadow DOM', function (done) {
374+
axe.run('#shadow-host', sConfig, function (err, results) {
375+
assert.isNull(err);
376+
assert.lengthOf(results.violations, 2, 'violations');
377+
var allowedAttrsViolations = results.violations.filter(
378+
function (violation) {
379+
return violation.id === 'aria-allowed-attr';
380+
}
381+
);
382+
assert.lengthOf(
383+
allowedAttrsViolations,
384+
1,
385+
'aria allowed attrs violations'
386+
);
387+
done();
388+
});
389+
});
390+
it('when passed a shadow root, reports issues in the shadow DOM, but not on the host', function (done) {
391+
var host = document.querySelector('#shadow-host');
392+
var shadowRoot = host.shadowRoot;
393+
axe.run(shadowRoot, sConfig, function (err, results) {
394+
assert.isNull(err);
395+
assert.lengthOf(results.violations, 1, 'violations');
396+
var allowedAttrsViolations = results.violations.filter(
397+
function (violation) {
398+
return violation.id === 'aria-allowed-attr';
399+
}
400+
);
401+
assert.lengthOf(
402+
allowedAttrsViolations,
403+
0,
404+
'aria allowed attrs violations'
405+
);
406+
done();
407+
});
408+
});
409+
});
365410
});
366411

367412
describe('indirect include', function () {

0 commit comments

Comments
 (0)