From 624ddbdffc0eed484962dcf0558b6856eb9245d7 Mon Sep 17 00:00:00 2001 From: jkodu Date: Thu, 27 Sep 2018 14:46:19 +0100 Subject: [PATCH 01/18] feat: new rule aria-hidden-focus --- doc/rule-descriptions.md | 1 + lib/checks/aria/aria-hidden-focus.js | 15 ++++ lib/checks/aria/aria-hidden-focus.json | 11 +++ lib/commons/dom/is-focusable.js | 11 +-- lib/rules/aria-hidden-focus.json | 19 ++++ test/checks/aria/aria-hidden-focus.js | 86 +++++++++++++++++++ .../aria-hidden-focus/aria-hidden-focus.html | 24 ++++++ .../aria-hidden-focus/aria-hidden-focus.json | 38 ++++++++ 8 files changed, 200 insertions(+), 5 deletions(-) create mode 100755 lib/checks/aria/aria-hidden-focus.js create mode 100755 lib/checks/aria/aria-hidden-focus.json create mode 100755 lib/rules/aria-hidden-focus.json create mode 100755 test/checks/aria/aria-hidden-focus.js create mode 100644 test/integration/rules/aria-hidden-focus/aria-hidden-focus.html create mode 100644 test/integration/rules/aria-hidden-focus/aria-hidden-focus.json diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 88dfac245c..bf9bf912a3 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -6,6 +6,7 @@ | aria-allowed-role | Ensures role attribute has an appropriate value for the element | Minor | cat.aria, best-practice | true | | aria-dpub-role-fallback | Ensures unsupported DPUB roles are only used on elements with implicit fallback roles | Moderate | cat.aria, wcag2a, wcag131 | true | | aria-hidden-body | Ensures aria-hidden='true' is not present on the document body. | Critical | cat.aria, wcag2a, wcag412 | true | +| aria-hidden-focus | Ensures aria-hidden element do not contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412 | true | | aria-required-attr | Ensures elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | true | | aria-required-children | Ensures elements with an ARIA role that require child roles contain them | Critical | cat.aria, wcag2a, wcag131 | true | | aria-required-parent | Ensures elements with an ARIA role that require parent roles are contained by them | Critical | cat.aria, wcag2a, wcag131 | true | diff --git a/lib/checks/aria/aria-hidden-focus.js b/lib/checks/aria/aria-hidden-focus.js new file mode 100755 index 0000000000..05dddf943e --- /dev/null +++ b/lib/checks/aria/aria-hidden-focus.js @@ -0,0 +1,15 @@ +const { dom } = axe.commons; + +let elements = [node]; + +if (node.children.length) { + const children = Array.prototype.slice.call(node.querySelectorAll('*')); + elements = elements.concat(children); +} + +const result = elements.every(element => { + const output = dom.isFocusable(element, false) === false; + return output; +}); + +return result; diff --git a/lib/checks/aria/aria-hidden-focus.json b/lib/checks/aria/aria-hidden-focus.json new file mode 100755 index 0000000000..0bb501ae90 --- /dev/null +++ b/lib/checks/aria/aria-hidden-focus.json @@ -0,0 +1,11 @@ +{ + "id": "aria-hidden-focus", + "evaluate": "aria-hidden-focus.js", + "metadata": { + "impact": "serious", + "messages": { + "pass": "No focusable elements under aria-hidden element", + "fail": "aria-hidden=true element must not contain focusable elements" + } + } +} diff --git a/lib/commons/dom/is-focusable.js b/lib/commons/dom/is-focusable.js index 060a9a5eb6..19348f6326 100644 --- a/lib/commons/dom/is-focusable.js +++ b/lib/commons/dom/is-focusable.js @@ -4,12 +4,13 @@ /** * Determines if focusing has been disabled on an element. * @param {HTMLElement} el The HTMLElement + * @param {Boolean} screenReader Default(true), When provided, will evaluate visibility from the perspective of a screen reader * @return {Boolean} Whether focusing has been disabled on an element. */ -function focusDisabled(el) { +function focusDisabled(el, screenReader = true) { return ( el.disabled || - (!dom.isVisible(el, true) && el.nodeName.toUpperCase() !== 'AREA') + (!dom.isVisible(el, screenReader) && el.nodeName.toUpperCase() !== 'AREA') ); } @@ -19,12 +20,12 @@ function focusDisabled(el) { * @memberof axe.commons.dom * @instance * @param {HTMLElement} el The HTMLElement + * @param {Boolean} screenReader Default(true), When provided, will evaluate visibility from the perspective of a screen reader * @return {Boolean} The element's focusability status */ -dom.isFocusable = function(el) { - 'use strict'; - if (focusDisabled(el)) { +dom.isFocusable = function(el, screenReader = true) { + if (focusDisabled(el, screenReader)) { return false; } else if (dom.isNativelyFocusable(el)) { return true; diff --git a/lib/rules/aria-hidden-focus.json b/lib/rules/aria-hidden-focus.json new file mode 100755 index 0000000000..d6dc731e5c --- /dev/null +++ b/lib/rules/aria-hidden-focus.json @@ -0,0 +1,19 @@ +{ + "id": "aria-hidden-focus", + "selector": "[aria-hidden=\"true\"]", + "excludeHidden": false, + "tags": [ + "cat.name-role-value", + "wcag2a", + "wcag412" + ], + "metadata": { + "description": "Ensures aria-hidden element do not contain focusable elements", + "help": "ARIA hidden element must not contain focusable elements" + }, + "all": [], + "any": [ + "aria-hidden-focus" + ], + "none": [] +} diff --git a/test/checks/aria/aria-hidden-focus.js b/test/checks/aria/aria-hidden-focus.js new file mode 100755 index 0000000000..a7193fe595 --- /dev/null +++ b/test/checks/aria/aria-hidden-focus.js @@ -0,0 +1,86 @@ +describe('aria-hidden-focus', function() { + 'use strict'; + + var fixture = document.getElementById('fixture'); + var check = undefined; + var checkContext = axe.testUtils.MockCheckContext(); + + before(function() { + check = checks['aria-hidden-focus']; + checkContext._data = null; + }); + + afterEach(function() { + fixture.innerHTML = ''; + }); + + it('returns true when content not focusable by default', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + var actual = check.evaluate.call(checkContext, node); + assert.isTrue(actual); + }); + + it('returns true when content hidden through CSS', function() { + fixture.innerHTML = + ''; + var node = fixture.querySelector('#target'); + var actual = check.evaluate.call(checkContext, node); + assert.isTrue(actual); + }); + + it('returns true when content made unfocusable through tabindex', function() { + fixture.innerHTML = + ''; + var node = fixture.querySelector('#target'); + var actual = check.evaluate.call(checkContext, node); + assert.isTrue(actual); + }); + + it('returns true when content made unfocusable through disabled', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + var actual = check.evaluate.call(checkContext, node); + assert.isTrue(actual); + }); + + it('returns false when focusable off screen link', function() { + fixture.innerHTML = + ''; + var node = fixture.querySelector('#target'); + var actual = check.evaluate.call(checkContext, node); + assert.isFalse(actual); + }); + + it('returns false when focusable form field, incorrectly disabled', function() { + fixture.innerHTML = + ''; + var node = fixture.querySelector('#target'); + var actual = check.evaluate.call(checkContext, node); + assert.isFalse(actual); + }); + + it('returns false when aria-hidden=false does not negate aria-hidden true', function() { + fixture.innerHTML = + ''; + var node = fixture.querySelector('#target'); + var actual = check.evaluate.call(checkContext, node); + assert.isFalse(actual); + }); + + it('returns false when focusable content through tabindex', function() { + fixture.innerHTML = + ''; + var node = fixture.querySelector('#target'); + var actual = check.evaluate.call(checkContext, node); + assert.isFalse(actual); + }); + + it('returns false when Focusable summary element', function() { + fixture.innerHTML = + ''; + var node = fixture.querySelector('#target'); + var actual = check.evaluate.call(checkContext, node); + assert.isFalse(actual); + }); +}); diff --git a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html new file mode 100644 index 0000000000..29aa3d70db --- /dev/null +++ b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.json b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.json new file mode 100644 index 0000000000..d5c0afd605 --- /dev/null +++ b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.json @@ -0,0 +1,38 @@ +{ + "description": "aria-hidden-focus tests", + "rule": "aria-hidden-focus", + "violations": [ + [ + "#violation1" + ], + [ + "#violation2" + ], + [ + "#violation3" + ], + [ + "#violation4" + ], + [ + "#violation5" + ] + ], + "passes": [ + [ + "#pass1" + ], + [ + "#pass2" + ], + [ + "#pass3" + ], + [ + "#pass4" + ], + [ + "#pass5" + ] + ] +} From 212d0da7d826007146c8a203b30abade2b264353 Mon Sep 17 00:00:00 2001 From: jkodu Date: Sun, 28 Oct 2018 14:07:11 +0000 Subject: [PATCH 02/18] fix: revert changes to isFocusable. To be tackled by issue #1208 --- lib/commons/dom/is-focusable.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/commons/dom/is-focusable.js b/lib/commons/dom/is-focusable.js index 19348f6326..060a9a5eb6 100644 --- a/lib/commons/dom/is-focusable.js +++ b/lib/commons/dom/is-focusable.js @@ -4,13 +4,12 @@ /** * Determines if focusing has been disabled on an element. * @param {HTMLElement} el The HTMLElement - * @param {Boolean} screenReader Default(true), When provided, will evaluate visibility from the perspective of a screen reader * @return {Boolean} Whether focusing has been disabled on an element. */ -function focusDisabled(el, screenReader = true) { +function focusDisabled(el) { return ( el.disabled || - (!dom.isVisible(el, screenReader) && el.nodeName.toUpperCase() !== 'AREA') + (!dom.isVisible(el, true) && el.nodeName.toUpperCase() !== 'AREA') ); } @@ -20,12 +19,12 @@ function focusDisabled(el, screenReader = true) { * @memberof axe.commons.dom * @instance * @param {HTMLElement} el The HTMLElement - * @param {Boolean} screenReader Default(true), When provided, will evaluate visibility from the perspective of a screen reader * @return {Boolean} The element's focusability status */ -dom.isFocusable = function(el, screenReader = true) { - if (focusDisabled(el, screenReader)) { +dom.isFocusable = function(el) { + 'use strict'; + if (focusDisabled(el)) { return false; } else if (dom.isNativelyFocusable(el)) { return true; From 35816f55875c21102c15385a0e10d1903ca60573 Mon Sep 17 00:00:00 2001 From: jkodu Date: Thu, 8 Nov 2018 10:15:03 +0000 Subject: [PATCH 03/18] refactor: based on review comments --- doc/rule-descriptions.md | 2 +- lib/checks/aria/aria-hidden-focus.js | 22 +++++++++++++++------- lib/rules/aria-hidden-focus.json | 2 +- test/checks/aria/aria-hidden-focus.js | 2 ++ 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 537e83a41a..4dc6505a80 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -6,7 +6,7 @@ | aria-allowed-role | Ensures role attribute has an appropriate value for the element | Minor | cat.aria, best-practice | true | | aria-dpub-role-fallback | Ensures unsupported DPUB roles are only used on elements with implicit fallback roles | Moderate | cat.aria, wcag2a, wcag131 | true | | aria-hidden-body | Ensures aria-hidden='true' is not present on the document body. | Critical | cat.aria, wcag2a, wcag412 | true | -| aria-hidden-focus | Ensures aria-hidden element do not contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412 | true | +| aria-hidden-focus | Ensures aria-hidden elements do not contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412 | true | | aria-required-attr | Ensures elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | true | | aria-required-children | Ensures elements with an ARIA role that require child roles contain them | Critical | cat.aria, wcag2a, wcag131 | true | | aria-required-parent | Ensures elements with an ARIA role that require parent roles are contained by them | Critical | cat.aria, wcag2a, wcag131 | true | diff --git a/lib/checks/aria/aria-hidden-focus.js b/lib/checks/aria/aria-hidden-focus.js index 05dddf943e..5ecc2ff029 100755 --- a/lib/checks/aria/aria-hidden-focus.js +++ b/lib/checks/aria/aria-hidden-focus.js @@ -1,12 +1,20 @@ -const { dom } = axe.commons; - -let elements = [node]; - -if (node.children.length) { - const children = Array.prototype.slice.call(node.querySelectorAll('*')); - elements = elements.concat(children); +function getAllElmsIncludingShadowDOM(nodes, elms = []) { + // TODO: perhaps can be moved to a utility fn, when the need arises + for (let i = 0, el; (el = nodes[i]); ++i) { + elms.push(el); + if (axe.utils.isShadowRoot(el)) { + // recursive look up if elm has shadowRoot + getAllElmsIncludingShadowDOM(el.shadowRoot.querySelectorAll('*'), elms); + } + } + return elms; } +const { dom } = axe.commons; +let elements = [node].concat( + getAllElmsIncludingShadowDOM(node.querySelectorAll('*')) +); + const result = elements.every(element => { const output = dom.isFocusable(element, false) === false; return output; diff --git a/lib/rules/aria-hidden-focus.json b/lib/rules/aria-hidden-focus.json index d6dc731e5c..cfce71098d 100755 --- a/lib/rules/aria-hidden-focus.json +++ b/lib/rules/aria-hidden-focus.json @@ -8,7 +8,7 @@ "wcag412" ], "metadata": { - "description": "Ensures aria-hidden element do not contain focusable elements", + "description": "Ensures aria-hidden elements do not contain focusable elements", "help": "ARIA hidden element must not contain focusable elements" }, "all": [], diff --git a/test/checks/aria/aria-hidden-focus.js b/test/checks/aria/aria-hidden-focus.js index a7193fe595..3fab5f601c 100755 --- a/test/checks/aria/aria-hidden-focus.js +++ b/test/checks/aria/aria-hidden-focus.js @@ -14,6 +14,7 @@ describe('aria-hidden-focus', function() { fixture.innerHTML = ''; }); + // pass it('returns true when content not focusable by default', function() { fixture.innerHTML = ''; var node = fixture.querySelector('#target'); @@ -44,6 +45,7 @@ describe('aria-hidden-focus', function() { assert.isTrue(actual); }); + // fail it('returns false when focusable off screen link', function() { fixture.innerHTML = ''; From 861428a2a27ed3dbc39ed9cf37119d2dc3dab0d8 Mon Sep 17 00:00:00 2001 From: jkodu Date: Thu, 8 Nov 2018 12:08:18 +0000 Subject: [PATCH 04/18] fix: update aria-hidden focus check and add tests --- lib/checks/aria/aria-hidden-focus.js | 29 ++++++++++++------- test/checks/aria/aria-hidden-focus.js | 23 ++++++++------- .../aria-hidden-focus/aria-hidden-focus.html | 15 +++++----- .../aria-hidden-focus/aria-hidden-focus.json | 3 -- 4 files changed, 38 insertions(+), 32 deletions(-) diff --git a/lib/checks/aria/aria-hidden-focus.js b/lib/checks/aria/aria-hidden-focus.js index 5ecc2ff029..d489fe3b8d 100755 --- a/lib/checks/aria/aria-hidden-focus.js +++ b/lib/checks/aria/aria-hidden-focus.js @@ -1,23 +1,30 @@ -function getAllElmsIncludingShadowDOM(nodes, elms = []) { - // TODO: perhaps can be moved to a utility fn, when the need arises +function getElmsIncludingShadowDOM(nodes, elms = []) { + // Note: can be moved to a utility fn, when the need arises for (let i = 0, el; (el = nodes[i]); ++i) { elms.push(el); if (axe.utils.isShadowRoot(el)) { // recursive look up if elm has shadowRoot - getAllElmsIncludingShadowDOM(el.shadowRoot.querySelectorAll('*'), elms); + getElmsIncludingShadowDOM(el.shadowRoot.querySelectorAll('*'), elms); } } return elms; } -const { dom } = axe.commons; -let elements = [node].concat( - getAllElmsIncludingShadowDOM(node.querySelectorAll('*')) -); - -const result = elements.every(element => { - const output = dom.isFocusable(element, false) === false; - return output; +const children = getElmsIncludingShadowDOM(node.querySelectorAll('*')); +const elements = [node].concat(children); +const result = elements.every(el => { + const isElFocusable = axe.commons.dom.isFocusable(el); + let tabIndex = el.getAttribute('tabindex'); + tabIndex = + tabIndex && !isNaN(parseInt(tabIndex, 10)) ? parseInt(tabIndex) : null; + if (isElFocusable && (tabIndex && tabIndex < 0)) { + return true; + } + return isElFocusable === false; }); +if (!result) { + this.relatedNodes(children); +} + return result; diff --git a/test/checks/aria/aria-hidden-focus.js b/test/checks/aria/aria-hidden-focus.js index 3fab5f601c..2a5bdfca8e 100755 --- a/test/checks/aria/aria-hidden-focus.js +++ b/test/checks/aria/aria-hidden-focus.js @@ -32,7 +32,9 @@ describe('aria-hidden-focus', function() { it('returns true when content made unfocusable through tabindex', function() { fixture.innerHTML = - ''; + ''; var node = fixture.querySelector('#target'); var actual = check.evaluate.call(checkContext, node); assert.isTrue(actual); @@ -45,26 +47,27 @@ describe('aria-hidden-focus', function() { assert.isTrue(actual); }); - // fail - it('returns false when focusable off screen link', function() { + it('returns true when aria-hidden=false does not negate aria-hidden true', function() { + // Note: aria-hidden can't be reset once you've set it to true on an ancestor fixture.innerHTML = - ''; + ''; var node = fixture.querySelector('#target'); var actual = check.evaluate.call(checkContext, node); - assert.isFalse(actual); + assert.isTrue(actual); }); - it('returns false when focusable form field, incorrectly disabled', function() { + // fail + it('returns false when focusable off screen link', function() { fixture.innerHTML = - ''; + ''; var node = fixture.querySelector('#target'); var actual = check.evaluate.call(checkContext, node); assert.isFalse(actual); }); - it('returns false when aria-hidden=false does not negate aria-hidden true', function() { + it('returns false when focusable form field, incorrectly disabled', function() { fixture.innerHTML = - ''; + ''; var node = fixture.querySelector('#target'); var actual = check.evaluate.call(checkContext, node); assert.isFalse(actual); @@ -78,7 +81,7 @@ describe('aria-hidden-focus', function() { assert.isFalse(actual); }); - it('returns false when Focusable summary element', function() { + it('returns false when focusable summary element', function() { fixture.innerHTML = ''; var node = fixture.querySelector('#target'); diff --git a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html index 29aa3d70db..105734c209 100644 --- a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html +++ b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html @@ -3,7 +3,11 @@ - + ' + ); var node = fixture.querySelector('#target'); - var actual = check.evaluate.call(checkContext, node); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = check.evaluate.call(checkContext, node, {}, virtualNode); assert.isTrue(actual); }); it('returns true when content made unfocusable through disabled', function() { - fixture.innerHTML = ''; + fixtureSetup(''); var node = fixture.querySelector('#target'); - var actual = check.evaluate.call(checkContext, node); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = check.evaluate.call(checkContext, node, {}, virtualNode); assert.isTrue(actual); }); it('returns true when aria-hidden=false does not negate aria-hidden true', function() { // Note: aria-hidden can't be reset once you've set it to true on an ancestor - fixture.innerHTML = - ''; + fixtureSetup( + '' + ); var node = fixture.querySelector('#target'); - var actual = check.evaluate.call(checkContext, node); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = check.evaluate.call(checkContext, node, {}, virtualNode); assert.isTrue(actual); }); (shadowSupported ? it : xit)( 'returns true when content hidden through CSS inside shadowDOM', function() { - fixture.innerHTML = '
'; + fixtureSetup('
'); var node = fixture.querySelector('#target'); var shadow = node.attachShadow({ mode: 'open' }); shadow.innerHTML = ''; - var actual = check.evaluate.call(checkContext, node); + axe._tree = axe.utils.getFlattenedTree(fixture); + axe._selectorData = axe.utils.getSelectorData(axe._tree); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + assert.isTrue(actual); + } + ); + + (shadowSupported ? it : xit)( + 'returns true when BUTTON is removed from tab order through tabindex which coexists with plain text in shadowDOM', + function() { + fixtureSetup( + '`' + ); + var node = fixture.querySelector('#target'); + var shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = 'plain text'; + axe._tree = axe.utils.getFlattenedTree(fixture); + axe._selectorData = axe.utils.getSelectorData(axe._tree); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = check.evaluate.call(checkContext, node, {}, virtualNode); assert.isTrue(actual); } ); // fail it('returns false when focusable off screen link', function() { - fixture.innerHTML = - ''; + fixtureSetup( + '' + ); var node = fixture.querySelector('#target'); - var actual = check.evaluate.call(checkContext, node); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = check.evaluate.call(checkContext, node, {}, virtualNode); assert.isFalse(actual); + assert.deepEqual( + checkContext._relatedNodes, + Array.from(fixture.querySelectorAll('a')) + ); }); - it('returns false when focusable form field, incorrectly disabled', function() { - fixture.innerHTML = - ''; + it('returns false when focusable form field only disabled through ARIA', function() { + fixtureSetup( + '' + ); var node = fixture.querySelector('#target'); - var actual = check.evaluate.call(checkContext, node); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = check.evaluate.call(checkContext, node, {}, virtualNode); assert.isFalse(actual); + assert.deepEqual( + checkContext._relatedNodes, + Array.from(fixture.querySelectorAll('input')) + ); }); it('returns false when focusable content through tabindex', function() { - fixture.innerHTML = - ''; + fixtureSetup( + '' + ); + var node = fixture.querySelector('#target'); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + assert.isFalse(actual); + }); + + it('returns false when focusable SUMMARY element', function() { + fixtureSetup( + '' + ); + var node = fixture.querySelector('#target'); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + assert.isFalse(actual); + assert.deepEqual( + checkContext._relatedNodes, + Array.from(fixture.querySelectorAll('details')) + ); + }); + + it('returns false when focusable SELECT element', function() { + fixtureSetup( + '' + ); var node = fixture.querySelector('#target'); - var actual = check.evaluate.call(checkContext, node); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = check.evaluate.call(checkContext, node, {}, virtualNode); assert.isFalse(actual); + assert.deepEqual( + checkContext._relatedNodes, + Array.from(fixture.querySelectorAll('select')) + ); }); - it('returns false when focusable summary element', function() { - fixture.innerHTML = - ''; + it('returns false when focusable AREA element', function() { + fixtureSetup( + '
' + + '' + + 'Mozilla' + + '' + + '
' + ); var node = fixture.querySelector('#target'); - var actual = check.evaluate.call(checkContext, node); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = check.evaluate.call(checkContext, node, {}, virtualNode); assert.isFalse(actual); + assert.deepEqual( + checkContext._relatedNodes, + Array.from(fixture.querySelectorAll('area')) + ); }); (shadowSupported ? it : xit)( 'returns false when focusable content through tabindex inside shadowDOM', function() { - fixture.innerHTML = '
'; + fixtureSetup('
'); var node = fixture.querySelector('#target'); var shadow = node.attachShadow({ mode: 'open' }); - shadow.innerHTML = ''; - var actual = check.evaluate.call(checkContext, node); + shadow.innerHTML = ''; + axe._tree = axe.utils.getFlattenedTree(fixture); + axe._selectorData = axe.utils.getSelectorData(axe._tree); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = check.evaluate.call(checkContext, node, {}, virtualNode); assert.isFalse(actual); } ); diff --git a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html index 105734c209..e7fd6e55bb 100644 --- a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html +++ b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.html @@ -1,23 +1,65 @@ + - - - + + + + + + + + + + + + + + + + + + + +
+ + Mozilla + +
diff --git a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.json b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.json index 8e32ba28b2..1448545bb3 100644 --- a/test/integration/rules/aria-hidden-focus/aria-hidden-focus.json +++ b/test/integration/rules/aria-hidden-focus/aria-hidden-focus.json @@ -13,6 +13,12 @@ ], [ "#violation4" + ], + [ + "#violation5" + ], + [ + "#violation6" ] ], "passes": [ @@ -30,6 +36,9 @@ ], [ "#pass5" + ], + [ + "#pass6" ] ] } From addaa3283fbb960c35c50641d96ca9ad3459aab3 Mon Sep 17 00:00:00 2001 From: jkodu Date: Wed, 14 Nov 2018 16:28:04 +0000 Subject: [PATCH 07/18] fix: update var name to retrigger build --- lib/checks/aria/aria-hidden-focus.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/checks/aria/aria-hidden-focus.js b/lib/checks/aria/aria-hidden-focus.js index 8d2f933d62..19cd100e4e 100755 --- a/lib/checks/aria/aria-hidden-focus.js +++ b/lib/checks/aria/aria-hidden-focus.js @@ -1,6 +1,8 @@ -const tree = axe.utils.querySelectorAll(virtualNode, '*'); +const domTree = axe.utils.querySelectorAll(virtualNode, '*'); + const relatedNodes = []; -const result = tree.every(({ actualNode: el }) => { + +const result = domTree.every(({ actualNode: el }) => { const isElFocusable = axe.commons.dom.isFocusable(el); let tabIndex = el.getAttribute('tabindex'); // dom.isFocusable does not check for tabindex From 16fc190bdcc799f2ebcd7483461749fe4e2d1ff9 Mon Sep 17 00:00:00 2001 From: jkodu Date: Mon, 19 Nov 2018 16:16:15 +0000 Subject: [PATCH 08/18] fix: add matches checks and tests based on review --- lib/checks/aria/aria-hidden-focus.js | 18 ++-- lib/rules/aria-hidden-focus-matches.js | 16 +++ lib/rules/aria-hidden-focus.json | 1 + test/checks/aria/aria-hidden-focus.js | 100 ++++++------------ .../rule-matches/aria-hidden-focus-matches.js | 67 ++++++++++++ 5 files changed, 131 insertions(+), 71 deletions(-) create mode 100644 lib/rules/aria-hidden-focus-matches.js create mode 100644 test/rule-matches/aria-hidden-focus-matches.js diff --git a/lib/checks/aria/aria-hidden-focus.js b/lib/checks/aria/aria-hidden-focus.js index 19cd100e4e..b73ea94d92 100755 --- a/lib/checks/aria/aria-hidden-focus.js +++ b/lib/checks/aria/aria-hidden-focus.js @@ -1,12 +1,15 @@ -const domTree = axe.utils.querySelectorAll(virtualNode, '*'); +const descendents = axe.utils.querySelectorAll(virtualNode, '*'); const relatedNodes = []; -const result = domTree.every(({ actualNode: el }) => { +const nonFocusableElements = descendents.filter(({ actualNode: el }) => { const isElFocusable = axe.commons.dom.isFocusable(el); + + // Note: + // although `dom.isFocusable` checks for tabindex validity, it does not return the tabindex value + // we need hhe value of the tabindex to compare with focusable element + // in order to compute if a given `el` has been taken out of tab order. let tabIndex = el.getAttribute('tabindex'); - // dom.isFocusable does not check for tabindex - // this checks if a given `el` has been taken out of tab order tabIndex = tabIndex && !isNaN(parseInt(tabIndex, 10)) ? parseInt(tabIndex) : null; if (isElFocusable && (tabIndex && tabIndex < 0)) { @@ -15,13 +18,16 @@ const result = domTree.every(({ actualNode: el }) => { if (isElFocusable === false) { return true; } - // add to related nodes relatedNodes.push(el); return false; }); +if (nonFocusableElements.length === descendents.length) { + return true; +} + if (relatedNodes.length) { this.relatedNodes(relatedNodes); } -return result; +return false; diff --git a/lib/rules/aria-hidden-focus-matches.js b/lib/rules/aria-hidden-focus-matches.js new file mode 100644 index 0000000000..c6567462ec --- /dev/null +++ b/lib/rules/aria-hidden-focus-matches.js @@ -0,0 +1,16 @@ +const { getComposedParent } = axe.commons.dom; + +function shouldMatchElement(el) { + if (!el) { + return true; + } + const ariaHiddenValue = el.getAttribute('aria-hidden') + ? el.getAttribute('aria-hidden') + : null; + if (ariaHiddenValue === null) { + return shouldMatchElement(getComposedParent(el)); + } + return false; +} + +return shouldMatchElement(getComposedParent(node)); diff --git a/lib/rules/aria-hidden-focus.json b/lib/rules/aria-hidden-focus.json index cfce71098d..41b9c96050 100755 --- a/lib/rules/aria-hidden-focus.json +++ b/lib/rules/aria-hidden-focus.json @@ -1,6 +1,7 @@ { "id": "aria-hidden-focus", "selector": "[aria-hidden=\"true\"]", + "matches": "aria-hidden-focus-matches.js", "excludeHidden": false, "tags": [ "cat.name-role-value", diff --git a/test/checks/aria/aria-hidden-focus.js b/test/checks/aria/aria-hidden-focus.js index 5f7d9a6cc5..72126558d0 100755 --- a/test/checks/aria/aria-hidden-focus.js +++ b/test/checks/aria/aria-hidden-focus.js @@ -1,11 +1,12 @@ describe('aria-hidden-focus', function() { 'use strict'; + var check; var fixture = document.getElementById('fixture'); var fixtureSetup = axe.testUtils.fixtureSetup; - var check; var shadowSupported = axe.testUtils.shadowSupport.v1; var checkContext = axe.testUtils.MockCheckContext(); + var checkSetup = axe.testUtils.checkSetup; before(function() { check = checks['aria-hidden-focus']; @@ -20,87 +21,64 @@ describe('aria-hidden-focus', function() { // pass it('returns true when content not focusable by default', function() { - fixtureSetup(''); - var node = fixture.querySelector('#target'); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + var params = checkSetup(''); + var actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); it('returns true when content hidden through CSS', function() { - fixtureSetup( + var params = checkSetup( '' ); - var node = fixture.querySelector('#target'); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + var actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); it('returns true when BUTTON removed from tab order through tabindex', function() { - fixtureSetup( + var params = checkSetup( '' ); - var node = fixture.querySelector('#target'); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + var actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); it('returns true when TEXTAREA removed from tab order through tabindex', function() { - fixtureSetup( + var params = checkSetup( '' ); - var node = fixture.querySelector('#target'); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + var actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); it('returns true when content made unfocusable through disabled', function() { - fixtureSetup(''); - var node = fixture.querySelector('#target'); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + var params = checkSetup( + '' + ); + var actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); it('returns true when aria-hidden=false does not negate aria-hidden true', function() { // Note: aria-hidden can't be reset once you've set it to true on an ancestor - fixtureSetup( + var params = checkSetup( '' ); - var node = fixture.querySelector('#target'); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + var actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); - (shadowSupported ? it : xit)( - 'returns true when content hidden through CSS inside shadowDOM', - function() { - fixtureSetup('
'); - var node = fixture.querySelector('#target'); - var shadow = node.attachShadow({ mode: 'open' }); - shadow.innerHTML = - ''; - axe._tree = axe.utils.getFlattenedTree(fixture); - axe._selectorData = axe.utils.getSelectorData(axe._tree); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); - assert.isTrue(actual); - } - ); - (shadowSupported ? it : xit)( 'returns true when BUTTON is removed from tab order through tabindex which coexists with plain text in shadowDOM', function() { + // Note: + // `testUtils.checkSetup` does not work for shadowDOM + // as `axe._tree` and `axe._selectorData` needs to be updated after shadowDOM construction fixtureSetup( '`' ); @@ -117,12 +95,10 @@ describe('aria-hidden-focus', function() { // fail it('returns false when focusable off screen link', function() { - fixtureSetup( + var params = checkSetup( '' ); - var node = fixture.querySelector('#target'); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + var actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); assert.deepEqual( checkContext._relatedNodes, @@ -131,12 +107,10 @@ describe('aria-hidden-focus', function() { }); it('returns false when focusable form field only disabled through ARIA', function() { - fixtureSetup( + var params = checkSetup( '' ); - var node = fixture.querySelector('#target'); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + var actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); assert.deepEqual( checkContext._relatedNodes, @@ -145,22 +119,18 @@ describe('aria-hidden-focus', function() { }); it('returns false when focusable content through tabindex', function() { - fixtureSetup( + var params = checkSetup( '' ); - var node = fixture.querySelector('#target'); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + var actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); }); it('returns false when focusable SUMMARY element', function() { - fixtureSetup( + var params = checkSetup( '' ); - var node = fixture.querySelector('#target'); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + var actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); assert.deepEqual( checkContext._relatedNodes, @@ -169,7 +139,7 @@ describe('aria-hidden-focus', function() { }); it('returns false when focusable SELECT element', function() { - fixtureSetup( + var params = checkSetup( '' + ); + var actual = check.evaluate.apply(checkContext, params); + assert.isTrue(actual); + }); + + it('returns true when aria-hidden=false does not negate aria-hidden true', function() { + // Note: aria-hidden can't be reset once you've set it to true on an ancestor + var params = checkSetup( + '' + ); + var actual = check.evaluate.apply(checkContext, params); + assert.isTrue(actual); + }); + + (shadowSupported ? it : xit)( + 'returns true when BUTTON is removed from tab order in shadowDOM', + function() { + // Note: + // `testUtils.checkSetup` does not work for shadowDOM + // as `axe._tree` and `axe._selectorData` needs to be updated after shadowDOM construction + fixtureSetup('`'); + var node = fixture.querySelector('#target'); + var shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = ''; + axe._tree = axe.utils.getFlattenedTree(fixture); + axe._selectorData = axe.utils.getSelectorData(axe._tree); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = check.evaluate.call(checkContext, node, {}, virtualNode); + assert.isTrue(actual); + } + ); + + it('returns false when focusable content through tabindex', function() { + var params = checkSetup( + '' + ); + var actual = check.evaluate.apply(checkContext, params); + assert.isFalse(actual); + }); +}); diff --git a/test/checks/aria/aria-hidden-focus.js b/test/checks/shared/not-focusable.js old mode 100755 new mode 100644 similarity index 58% rename from test/checks/aria/aria-hidden-focus.js rename to test/checks/shared/not-focusable.js index 72126558d0..ffa58cf22f --- a/test/checks/aria/aria-hidden-focus.js +++ b/test/checks/shared/not-focusable.js @@ -1,4 +1,4 @@ -describe('aria-hidden-focus', function() { +describe('not-focusable', function() { 'use strict'; var check; @@ -9,7 +9,7 @@ describe('aria-hidden-focus', function() { var checkSetup = axe.testUtils.checkSetup; before(function() { - check = checks['aria-hidden-focus']; + check = checks['not-focusable']; }); afterEach(function() { @@ -19,7 +19,6 @@ describe('aria-hidden-focus', function() { checkContext.reset(); }); - // pass it('returns true when content not focusable by default', function() { var params = checkSetup(''); var actual = check.evaluate.apply(checkContext, params); @@ -34,28 +33,6 @@ describe('aria-hidden-focus', function() { assert.isTrue(actual); }); - it('returns true when BUTTON removed from tab order through tabindex', function() { - var params = checkSetup( - '' - ); - var actual = check.evaluate.apply(checkContext, params); - assert.isTrue(actual); - }); - - it('returns true when TEXTAREA removed from tab order through tabindex', function() { - var params = checkSetup( - '' - ); - var actual = check.evaluate.apply(checkContext, params); - assert.isTrue(actual); - }); - it('returns true when content made unfocusable through disabled', function() { var params = checkSetup( '' @@ -64,35 +41,6 @@ describe('aria-hidden-focus', function() { assert.isTrue(actual); }); - it('returns true when aria-hidden=false does not negate aria-hidden true', function() { - // Note: aria-hidden can't be reset once you've set it to true on an ancestor - var params = checkSetup( - '' - ); - var actual = check.evaluate.apply(checkContext, params); - assert.isTrue(actual); - }); - - (shadowSupported ? it : xit)( - 'returns true when BUTTON is removed from tab order through tabindex which coexists with plain text in shadowDOM', - function() { - // Note: - // `testUtils.checkSetup` does not work for shadowDOM - // as `axe._tree` and `axe._selectorData` needs to be updated after shadowDOM construction - fixtureSetup( - '`' - ); - var node = fixture.querySelector('#target'); - var shadow = node.attachShadow({ mode: 'open' }); - shadow.innerHTML = 'plain text'; - axe._tree = axe.utils.getFlattenedTree(fixture); - axe._selectorData = axe.utils.getSelectorData(axe._tree); - var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); - var actual = check.evaluate.call(checkContext, node, {}, virtualNode); - assert.isTrue(actual); - } - ); - // fail it('returns false when focusable off screen link', function() { var params = checkSetup( @@ -100,10 +48,7 @@ describe('aria-hidden-focus', function() { ); var actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); - assert.deepEqual( - checkContext._relatedNodes, - Array.from(fixture.querySelectorAll('a')) - ); + assert.isTrue(checkContext._relatedNodes.length === 0); }); it('returns false when focusable form field only disabled through ARIA', function() { @@ -118,24 +63,13 @@ describe('aria-hidden-focus', function() { ); }); - it('returns false when focusable content through tabindex', function() { - var params = checkSetup( - '' - ); - var actual = check.evaluate.apply(checkContext, params); - assert.isFalse(actual); - }); - it('returns false when focusable SUMMARY element', function() { var params = checkSetup( '' ); var actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); - assert.deepEqual( - checkContext._relatedNodes, - Array.from(fixture.querySelectorAll('details')) - ); + assert.isTrue(checkContext._relatedNodes.length === 0); }); it('returns false when focusable SELECT element', function() { @@ -166,17 +100,12 @@ describe('aria-hidden-focus', function() { '' + '' ); - var actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); - assert.deepEqual( - checkContext._relatedNodes, - Array.from(fixture.querySelectorAll('area')) - ); }); (shadowSupported ? it : xit)( - 'returns false when focusable content through tabindex inside shadowDOM', + 'returns false when focusable content inside shadowDOM', function() { // Note: // `testUtils.checkSetup` does not work for shadowDOM diff --git a/test/commons/dom/has-focusable-elements.js b/test/commons/dom/has-focusable-elements.js new file mode 100644 index 0000000000..5414553c10 --- /dev/null +++ b/test/commons/dom/has-focusable-elements.js @@ -0,0 +1,93 @@ +describe('dom.getFocusableElements', function() { + 'use strict'; + + var fixtureSetup = axe.testUtils.fixtureSetup; + var shadowSupported = axe.testUtils.shadowSupport.v1; + var getFocusableElementsFn = axe.commons.dom.getFocusableElements; + + afterEach(function() { + document.getElementById('fixture').innerHTML = ''; + }); + + it('returns true when element contains focusable element', function() { + var fixture = fixtureSetup( + '
' + + '' + + '
' + ); + var node = fixture.querySelector('#target'); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = getFocusableElementsFn(virtualNode); + assert.isTrue(actual); + }); + + it('returns false when element contains disabled (focusable) element', function() { + var fixture = fixtureSetup( + '
' + '' + '
' + ); + var node = fixture.querySelector('#target'); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = getFocusableElementsFn(virtualNode); + assert.isFalse(actual); + }); + + it('returns false when element does not contain focusable element', function() { + var fixture = fixtureSetup( + '
' + '

Some text

' + '
' + ); + var node = fixture.querySelector('#target'); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = getFocusableElementsFn(virtualNode); + assert.isFalse(actual); + }); + + (shadowSupported ? it : xit)( + 'returns true when element contains focusable element inside shadowDOM', + function() { + var fixture = fixtureSetup('
`'); + var node = fixture.querySelector('#target'); + var shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = ''; + // re build tree after shadowDOM is constructed + axe._tree = axe.utils.getFlattenedTree(fixture); + axe._selectorData = axe.utils.getSelectorData(axe._tree); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = getFocusableElementsFn(virtualNode); + assert.isTrue(actual); + } + ); + + (shadowSupported ? it : xit)( + 'returns false when element contains disabled (focusable) element inside shadowDOM', + function() { + var fixture = fixtureSetup('
`'); + var node = fixture.querySelector('#target'); + var shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = ''; + // re build tree after shadowDOM is constructed + axe._tree = axe.utils.getFlattenedTree(fixture); + axe._selectorData = axe.utils.getSelectorData(axe._tree); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = getFocusableElementsFn(virtualNode); + assert.isFalse(actual); + } + ); + + (shadowSupported ? it : xit)( + 'returns false when element does not contain focusable element inside shadowDOM', + function() { + var fixture = fixtureSetup('
`'); + var node = fixture.querySelector('#target'); + var shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = '

I am not focusable

'; + // re build tree after shadowDOM is constructed + axe._tree = axe.utils.getFlattenedTree(fixture); + axe._selectorData = axe.utils.getSelectorData(axe._tree); + var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node); + var actual = getFocusableElementsFn(virtualNode); + assert.isFalse(actual); + } + ); +}); From 1a6a9b701bbbbac5507e8717201257f8d049017c Mon Sep 17 00:00:00 2001 From: jkodu Date: Thu, 22 Nov 2018 08:10:29 +0000 Subject: [PATCH 10/18] test: update assertions and fix breaking tests --- test/checks/shared/focusable-not-tabbable.js | 1 + test/checks/shared/not-focusable.js | 6 ++++-- ...ble-elements.js => get-focusable-elements.js} | 16 +++++++++------- 3 files changed, 14 insertions(+), 9 deletions(-) rename test/commons/dom/{has-focusable-elements.js => get-focusable-elements.js} (88%) diff --git a/test/checks/shared/focusable-not-tabbable.js b/test/checks/shared/focusable-not-tabbable.js index 8c42215a29..0a09be34d2 100644 --- a/test/checks/shared/focusable-not-tabbable.js +++ b/test/checks/shared/focusable-not-tabbable.js @@ -37,6 +37,7 @@ describe('focusable-not-tabbable', function() { ); var actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); + assert.lengthOf(checkContext._relatedNodes, 1); assert.deepEqual( checkContext._relatedNodes, Array.from(fixture.querySelectorAll('a')) diff --git a/test/checks/shared/not-focusable.js b/test/checks/shared/not-focusable.js index ffa58cf22f..dba58bea4a 100644 --- a/test/checks/shared/not-focusable.js +++ b/test/checks/shared/not-focusable.js @@ -48,7 +48,7 @@ describe('not-focusable', function() { ); var actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); - assert.isTrue(checkContext._relatedNodes.length === 0); + assert.lengthOf(checkContext._relatedNodes, 0); }); it('returns false when focusable form field only disabled through ARIA', function() { @@ -57,6 +57,7 @@ describe('not-focusable', function() { ); var actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); + assert.lengthOf(checkContext._relatedNodes, 1); assert.deepEqual( checkContext._relatedNodes, Array.from(fixture.querySelectorAll('input')) @@ -69,7 +70,7 @@ describe('not-focusable', function() { ); var actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); - assert.isTrue(checkContext._relatedNodes.length === 0); + assert.lengthOf(checkContext._relatedNodes, 0); }); it('returns false when focusable SELECT element', function() { @@ -85,6 +86,7 @@ describe('not-focusable', function() { ); var actual = check.evaluate.apply(checkContext, params); assert.isFalse(actual); + assert.lengthOf(checkContext._relatedNodes, 1); assert.deepEqual( checkContext._relatedNodes, Array.from(fixture.querySelectorAll('select')) diff --git a/test/commons/dom/has-focusable-elements.js b/test/commons/dom/get-focusable-elements.js similarity index 88% rename from test/commons/dom/has-focusable-elements.js rename to test/commons/dom/get-focusable-elements.js index 5414553c10..dee4949ad8 100644 --- a/test/commons/dom/has-focusable-elements.js +++ b/test/commons/dom/get-focusable-elements.js @@ -9,7 +9,7 @@ describe('dom.getFocusableElements', function() { document.getElementById('fixture').innerHTML = ''; }); - it('returns true when element contains focusable element', function() { + it('returns true when element contains focusable element (ignores tabindex)', function() { var fixture = fixtureSetup( '
' + '