From f729e25300bc82a2679870e653b13f4c3aeee31a Mon Sep 17 00:00:00 2001 From: Marcy Sutton Date: Thu, 13 Jul 2017 16:31:40 -0700 Subject: [PATCH] feat: add shadow support to aria-required-children Closes https://github.com/dequelabs/axe-core/issues/421 --- lib/checks/aria/required-children.js | 12 +-- test/checks/aria/required-children.js | 129 +++++++++++++++++--------- 2 files changed, 93 insertions(+), 48 deletions(-) diff --git a/lib/checks/aria/required-children.js b/lib/checks/aria/required-children.js index 6d61ef5db5..73e30dd1e3 100644 --- a/lib/checks/aria/required-children.js +++ b/lib/checks/aria/required-children.js @@ -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 + '"]']; @@ -13,9 +13,8 @@ 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) { @@ -23,7 +22,8 @@ function ariaOwns(nodes, role) { 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; } } @@ -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); } diff --git a/test/checks/aria/required-children.js b/test/checks/aria/required-children.js index 57cdb5046c..46296572c4 100644 --- a/test/checks/aria/required-children.js +++ b/test/checks/aria/required-children.js @@ -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, @@ -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 = '

Nothing here.

'; - var node = fixture.querySelector('#target'); - assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node)); + var params = checkSetup('

Nothing here.

'); + + 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 = '
'; + + var target = document.querySelector('#target'); + var shadowRoot = target.attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = '

Nothing here.

'; + + 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 = '

Nothing here.

'; - var node = fixture.querySelector('#target'); - assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node)); + var params = checkSetup('

Nothing here.

'); + + 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 = '
'; + + var target = document.querySelector('#target'); + var shadowRoot = target.attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = '

Nothing here.

'; + + 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 = '

Nothing here.

'; - var node = fixture.querySelector('#target'); - assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node)); + var params = checkSetup('

Nothing here.

'); + 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 = '

Nothing here.

'; - var node = fixture.querySelector('#target'); - assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node)); + var params = checkSetup('

Nothing here.

'); + 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 = '

Nothing here.

Textbox

'; - var node = fixture.querySelector('#target'); - assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node)); + var params = checkSetup('

Nothing here.

Textbox

'); + 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 = '
'; + + var target = document.querySelector('#target'); + var shadowRoot = target.attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = '

Nothing here.

Textbox

'; + + 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 = '
Nothing here.
'; - var node = fixture.querySelector('#target'); - assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node)); + var params = checkSetup('
Nothing here.
'); + 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 = '
'; - var node = fixture.querySelector('#target'); - assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node)); + var params = checkSetup('
'); + assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params)); }); it('should pass one existing aria-owned child when one required', function () { - fixture.innerHTML = '

Nothing here.

'; - var node = fixture.querySelector('#target'); - assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node)); + var params = checkSetup('

Nothing here.

'); + assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params)); }); it('should pass one existing required child when one required', function () { - fixture.innerHTML = '

Nothing here.

'; - var node = fixture.querySelector('#target'); - assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node)); + var params = checkSetup('

Nothing here.

'); + 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 = '

Nothing here.

'; - var node = fixture.querySelector('#target'); - assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node)); + var params = checkSetup('

Nothing here.

'); + 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 = '
Nothing here.
'; - var node = fixture.querySelector('#target'); - assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node)); + var params = checkSetup('
Nothing here.
'); + assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params)); }); it('should pass direct existing required children', function () { - fixture.innerHTML = '

Nothing here.

'; - var node = fixture.querySelector('#target'); - assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node)); + var params = checkSetup('

Nothing here.

'); + assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params)); }); it('should pass indirect required children', function () { - fixture.innerHTML = '

Just a regular ol p that contains a...

Nothing here.

'; - var node = fixture.querySelector('#target'); - assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node)); + var params = checkSetup('

Just a regular ol p that contains a...

Nothing here.

'); + assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params)); }); it('should return true when a role has no required owned', function () { - fixture.innerHTML = '

Nothing here.

'; - var node = fixture.querySelector('#target'); - assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node)); + var params = checkSetup('

Nothing here.

'); + assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params)); }); });