Skip to content

Commit

Permalink
feat: add shadow support to aria-required-children
Browse files Browse the repository at this point in the history
Closes #421
  • Loading branch information
Marcy Sutton authored and marcysutton committed Jul 19, 2017
1 parent 7ea8d6b commit f729e25
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 48 deletions.
12 changes: 6 additions & 6 deletions lib/checks/aria/required-children.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ implicitNodes = axe.commons.aria.implicitNodes,
matchesSelector = axe.commons.utils.matchesSelector,
idrefs = axe.commons.dom.idrefs;

function owns(node, role, ariaOwned) {
function owns(node, virtualTree, role, ariaOwned) {
if (node === null) { return false; }
var implicit = implicitNodes(role),
selector = ['[role="' + role + '"]'];
Expand All @@ -13,17 +13,17 @@ function owns(node, role, ariaOwned) {
}

selector = selector.join(',');

return ariaOwned ? (matchesSelector(node, selector) || !!node.querySelector(selector)) :
!!node.querySelector(selector);
return ariaOwned ? (matchesSelector(node, selector) || !!axe.utils.querySelectorAll(virtualTree, selector)[0]) :
!!axe.utils.querySelectorAll(virtualTree, selector)[0];
}

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

for (index = 0, length = nodes.length; index < length; index++) {
if (nodes[index] === null) { continue; }
if (owns(nodes[index], role, true)) {
let virtualTree = axe.utils.getFlattenedTree(nodes[index]);
if (owns(nodes[index], virtualTree, role, true)) {
return true;
}
}
Expand All @@ -39,7 +39,7 @@ function missingRequiredChildren(node, childRoles, all) {

for (i = 0; i < l; i++) {
var r = childRoles[i];
if (owns(node, r) || ariaOwns(ownedElements, r)) {
if (owns(node, virtualNode, r) || ariaOwns(ownedElements, r)) {
if (!all) { return null; }
} else {
if (all) { missing.push(r); }
Expand Down
129 changes: 87 additions & 42 deletions test/checks/aria/required-children.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ describe('aria-required-children', function () {
'use strict';

var fixture = document.getElementById('fixture');
var shadowSupported = axe.testUtils.shadowSupport.v1;

var checkContext = {
_data: null,
Expand All @@ -10,97 +11,141 @@ describe('aria-required-children', function () {
}
};

function checkSetup (html, options, target) {
fixture.innerHTML = html;
axe._tree = axe.utils.getFlattenedTree(fixture);
var node = fixture.querySelector(target || '#target');
var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node);
return [node, options, virtualNode];
}

afterEach(function () {
fixture.innerHTML = '';
axe._tree = undefined;
checkContext._data = null;
});

it('should detect missing sole required child', function () {
fixture.innerHTML = '<div role="list" id="target"><p>Nothing here.</p></div>';
var node = fixture.querySelector('#target');
assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node));
var params = checkSetup('<div role="list" id="target"><p>Nothing here.</p></div>');

assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, ['listitem']);
});

(shadowSupported ? it : xit)
('should detect missing sole required child in shadow tree', function () {
fixture.innerHTML = '<div id="target" role="list"></div>';

var target = document.querySelector('#target');
var shadowRoot = target.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = '<p>Nothing here.</p>';

var tree = axe._tree = axe.utils.getFlattenedTree(fixture);
var virtualTarget = axe.utils.getNodeFromTree(tree[0], target);

var params = [target, undefined, virtualTarget];
assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, ['listitem']);
});

it('should detect multiple missing required children when one required', function () {
fixture.innerHTML = '<div role="grid" id="target"><p>Nothing here.</p></div>';
var node = fixture.querySelector('#target');
assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node));
var params = checkSetup('<div role="grid" id="target"><p>Nothing here.</p></div>');

assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, ['rowgroup', 'row']);
});

(shadowSupported ? it : xit)
('should detect missing multiple required children in shadow tree when one required', function () {
fixture.innerHTML = '<div role="grid" id="target"></div>';

var target = document.querySelector('#target');
var shadowRoot = target.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = '<p>Nothing here.</p>';

var tree = axe._tree = axe.utils.getFlattenedTree(fixture);
var virtualTarget = axe.utils.getNodeFromTree(tree[0], target);

var params = [target, undefined, virtualTarget];
assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, ['rowgroup', 'row']);
});

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

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

it('should pass all existing required children when all required', function () {
fixture.innerHTML = '<div role="combobox" id="target"><p role="listbox">Nothing here.</p><p role="textbox">Textbox</p></div>';
var node = fixture.querySelector('#target');
assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
var params = checkSetup('<div role="combobox" id="target"><p role="listbox">Nothing here.</p><p role="textbox">Textbox</p></div>');
assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
});

(shadowSupported ? it : xit)
('should pass all existing required children in shadow tree when all required', function () {
fixture.innerHTML = '<div role="combobox" id="target"></div>';

var target = document.querySelector('#target');
var shadowRoot = target.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = '<p role="listbox">Nothing here.</p><p role="textbox">Textbox</p>';

var tree = axe._tree = axe.utils.getFlattenedTree(fixture);
var virtualTarget = axe.utils.getNodeFromTree(tree[0], target);

var params = [target, undefined, virtualTarget];
assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
});

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

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

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

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

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

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

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

it('should pass indirect required children', function () {
fixture.innerHTML = '<div role="list" id="target"><p>Just a regular ol p that contains a... <p role="listitem">Nothing here.</p></p></div>';
var node = fixture.querySelector('#target');
assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
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>');
assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
});

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

});

0 comments on commit f729e25

Please sign in to comment.