Skip to content

Commit f729e25

Browse files
Marcy Suttonmarcysutton
authored andcommitted
feat: add shadow support to aria-required-children
Closes #421
1 parent 7ea8d6b commit f729e25

File tree

2 files changed

+93
-48
lines changed

2 files changed

+93
-48
lines changed

lib/checks/aria/required-children.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ implicitNodes = axe.commons.aria.implicitNodes,
33
matchesSelector = axe.commons.utils.matchesSelector,
44
idrefs = axe.commons.dom.idrefs;
55

6-
function owns(node, role, ariaOwned) {
6+
function owns(node, virtualTree, role, ariaOwned) {
77
if (node === null) { return false; }
88
var implicit = implicitNodes(role),
99
selector = ['[role="' + role + '"]'];
@@ -13,17 +13,17 @@ function owns(node, role, ariaOwned) {
1313
}
1414

1515
selector = selector.join(',');
16-
17-
return ariaOwned ? (matchesSelector(node, selector) || !!node.querySelector(selector)) :
18-
!!node.querySelector(selector);
16+
return ariaOwned ? (matchesSelector(node, selector) || !!axe.utils.querySelectorAll(virtualTree, selector)[0]) :
17+
!!axe.utils.querySelectorAll(virtualTree, selector)[0];
1918
}
2019

2120
function ariaOwns(nodes, role) {
2221
var index, length;
2322

2423
for (index = 0, length = nodes.length; index < length; index++) {
2524
if (nodes[index] === null) { continue; }
26-
if (owns(nodes[index], role, true)) {
25+
let virtualTree = axe.utils.getFlattenedTree(nodes[index]);
26+
if (owns(nodes[index], virtualTree, role, true)) {
2727
return true;
2828
}
2929
}
@@ -39,7 +39,7 @@ function missingRequiredChildren(node, childRoles, all) {
3939

4040
for (i = 0; i < l; i++) {
4141
var r = childRoles[i];
42-
if (owns(node, r) || ariaOwns(ownedElements, r)) {
42+
if (owns(node, virtualNode, r) || ariaOwns(ownedElements, r)) {
4343
if (!all) { return null; }
4444
} else {
4545
if (all) { missing.push(r); }

test/checks/aria/required-children.js

Lines changed: 87 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ describe('aria-required-children', function () {
22
'use strict';
33

44
var fixture = document.getElementById('fixture');
5+
var shadowSupported = axe.testUtils.shadowSupport.v1;
56

67
var checkContext = {
78
_data: null,
@@ -10,97 +11,141 @@ describe('aria-required-children', function () {
1011
}
1112
};
1213

14+
function checkSetup (html, options, target) {
15+
fixture.innerHTML = html;
16+
axe._tree = axe.utils.getFlattenedTree(fixture);
17+
var node = fixture.querySelector(target || '#target');
18+
var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node);
19+
return [node, options, virtualNode];
20+
}
21+
1322
afterEach(function () {
1423
fixture.innerHTML = '';
24+
axe._tree = undefined;
1525
checkContext._data = null;
1626
});
1727

1828
it('should detect missing sole required child', function () {
19-
fixture.innerHTML = '<div role="list" id="target"><p>Nothing here.</p></div>';
20-
var node = fixture.querySelector('#target');
21-
assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node));
29+
var params = checkSetup('<div role="list" id="target"><p>Nothing here.</p></div>');
30+
31+
assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
32+
assert.deepEqual(checkContext._data, ['listitem']);
33+
});
34+
35+
(shadowSupported ? it : xit)
36+
('should detect missing sole required child in shadow tree', function () {
37+
fixture.innerHTML = '<div id="target" role="list"></div>';
38+
39+
var target = document.querySelector('#target');
40+
var shadowRoot = target.attachShadow({ mode: 'open' });
41+
shadowRoot.innerHTML = '<p>Nothing here.</p>';
42+
43+
var tree = axe._tree = axe.utils.getFlattenedTree(fixture);
44+
var virtualTarget = axe.utils.getNodeFromTree(tree[0], target);
45+
46+
var params = [target, undefined, virtualTarget];
47+
assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
2248
assert.deepEqual(checkContext._data, ['listitem']);
2349
});
2450

2551
it('should detect multiple missing required children when one required', function () {
26-
fixture.innerHTML = '<div role="grid" id="target"><p>Nothing here.</p></div>';
27-
var node = fixture.querySelector('#target');
28-
assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node));
52+
var params = checkSetup('<div role="grid" id="target"><p>Nothing here.</p></div>');
53+
54+
assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
55+
assert.deepEqual(checkContext._data, ['rowgroup', 'row']);
56+
});
57+
58+
(shadowSupported ? it : xit)
59+
('should detect missing multiple required children in shadow tree when one required', function () {
60+
fixture.innerHTML = '<div role="grid" id="target"></div>';
61+
62+
var target = document.querySelector('#target');
63+
var shadowRoot = target.attachShadow({ mode: 'open' });
64+
shadowRoot.innerHTML = '<p>Nothing here.</p>';
65+
66+
var tree = axe._tree = axe.utils.getFlattenedTree(fixture);
67+
var virtualTarget = axe.utils.getNodeFromTree(tree[0], target);
68+
69+
var params = [target, undefined, virtualTarget];
70+
assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
2971
assert.deepEqual(checkContext._data, ['rowgroup', 'row']);
3072
});
3173

3274
it('should detect multiple missing required children when all required', function () {
33-
fixture.innerHTML = '<div role="combobox" id="target"><p>Nothing here.</p></div>';
34-
var node = fixture.querySelector('#target');
35-
assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node));
75+
var params = checkSetup('<div role="combobox" id="target"><p>Nothing here.</p></div>');
76+
assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
3677
assert.deepEqual(checkContext._data, ['listbox', 'textbox']);
3778
});
3879

3980
it('should detect single missing required child when all required', function () {
40-
fixture.innerHTML = '<div role="combobox" id="target"><p role="listbox">Nothing here.</p></div>';
41-
var node = fixture.querySelector('#target');
42-
assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node));
81+
var params = checkSetup('<div role="combobox" id="target"><p role="listbox">Nothing here.</p></div>');
82+
assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
4383
assert.deepEqual(checkContext._data, ['textbox']);
4484
});
4585

4686
it('should pass all existing required children when all required', function () {
47-
fixture.innerHTML = '<div role="combobox" id="target"><p role="listbox">Nothing here.</p><p role="textbox">Textbox</p></div>';
48-
var node = fixture.querySelector('#target');
49-
assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
87+
var params = checkSetup('<div role="combobox" id="target"><p role="listbox">Nothing here.</p><p role="textbox">Textbox</p></div>');
88+
assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
89+
});
90+
91+
(shadowSupported ? it : xit)
92+
('should pass all existing required children in shadow tree when all required', function () {
93+
fixture.innerHTML = '<div role="combobox" id="target"></div>';
94+
95+
var target = document.querySelector('#target');
96+
var shadowRoot = target.attachShadow({ mode: 'open' });
97+
shadowRoot.innerHTML = '<p role="listbox">Nothing here.</p><p role="textbox">Textbox</p>';
98+
99+
var tree = axe._tree = axe.utils.getFlattenedTree(fixture);
100+
var virtualTarget = axe.utils.getNodeFromTree(tree[0], target);
101+
102+
var params = [target, undefined, virtualTarget];
103+
assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
50104
});
51105

52106
it('should pass one indirectly aria-owned child when one required', function () {
53-
fixture.innerHTML = '<div role="grid" id="target" aria-owns="r"></div><div id="r"><div role="row">Nothing here.</div></div>';
54-
var node = fixture.querySelector('#target');
55-
assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
107+
var params = checkSetup('<div role="grid" id="target" aria-owns="r"></div><div id="r"><div role="row">Nothing here.</div></div>');
108+
assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
56109
});
57110

58111
it('should not break if aria-owns points to non-existent node', function () {
59-
fixture.innerHTML = '<div role="grid" id="target" aria-owns="nonexistent"></div>';
60-
var node = fixture.querySelector('#target');
61-
assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node));
112+
var params = checkSetup('<div role="grid" id="target" aria-owns="nonexistent"></div>');
113+
assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
62114
});
63115

64116
it('should pass one existing aria-owned child when one required', function () {
65-
fixture.innerHTML = '<div role="grid" id="target" aria-owns="r"></div><p id="r" role="row">Nothing here.</p>';
66-
var node = fixture.querySelector('#target');
67-
assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
117+
var params = checkSetup('<div role="grid" id="target" aria-owns="r"></div><p id="r" role="row">Nothing here.</p>');
118+
assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
68119
});
69120

70121
it('should pass one existing required child when one required', function () {
71-
fixture.innerHTML = '<div role="grid" id="target"><p role="row">Nothing here.</p></div>';
72-
var node = fixture.querySelector('#target');
73-
assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
122+
var params = checkSetup('<div role="grid" id="target"><p role="row">Nothing here.</p></div>');
123+
assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
74124
});
75125

76126
it('should pass one existing required child when one required because of implicit role', function () {
77-
fixture.innerHTML = '<table id="target"><p role="row">Nothing here.</p></table>';
78-
var node = fixture.querySelector('#target');
79-
assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
127+
var params = checkSetup('<table id="target"><p role="row">Nothing here.</p></table>');
128+
assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
80129
});
81130

82131
it('should pass when a child with an implicit role is present', function () {
83-
fixture.innerHTML = '<table role="grid" id="target"><tr><td>Nothing here.</td></tr></table>';
84-
var node = fixture.querySelector('#target');
85-
assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
132+
var params = checkSetup('<table role="grid" id="target"><tr><td>Nothing here.</td></tr></table>');
133+
assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
86134
});
87135

88136
it('should pass direct existing required children', function () {
89-
fixture.innerHTML = '<div role="list" id="target"><p role="listitem">Nothing here.</p></div>';
90-
var node = fixture.querySelector('#target');
91-
assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
137+
var params = checkSetup('<div role="list" id="target"><p role="listitem">Nothing here.</p></div>');
138+
assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
92139
});
93140

94141
it('should pass indirect required children', function () {
95-
fixture.innerHTML = '<div role="list" id="target"><p>Just a regular ol p that contains a... <p role="listitem">Nothing here.</p></p></div>';
96-
var node = fixture.querySelector('#target');
97-
assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
142+
var params = checkSetup('<div role="list" id="target"><p>Just a regular ol p that contains a... <p role="listitem">Nothing here.</p></p></div>');
143+
assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
98144
});
99145

100146
it('should return true when a role has no required owned', function () {
101-
fixture.innerHTML = '<div role="listitem" id="target"><p>Nothing here.</p></div>';
102-
var node = fixture.querySelector('#target');
103-
assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
147+
var params = checkSetup('<div role="listitem" id="target"><p>Nothing here.</p></div>');
148+
assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
104149
});
105150

106151
});

0 commit comments

Comments
 (0)