diff --git a/lib/checks/navigation/region.js b/lib/checks/navigation/region.js index 653146f21f..e890bea5ab 100644 --- a/lib/checks/navigation/region.js +++ b/lib/checks/navigation/region.js @@ -1,17 +1,4 @@ const { dom, aria } = axe.commons; - -// Return the skplink, if any -function getSkiplink(virtualNode) { - const firstLink = axe.utils.querySelectorAll(virtualNode, 'a[href]')[0]; - if ( - firstLink && - axe.commons.dom.getElementByReference(firstLink.actualNode, 'href') - ) { - return firstLink.actualNode; - } -} - -const skipLink = getSkiplink(virtualNode); const landmarkRoles = aria.getRolesByType('landmark'); // Create a list of nodeNames that have a landmark as an implicit role @@ -19,11 +6,6 @@ const implicitLandmarks = landmarkRoles .reduce((arr, role) => arr.concat(aria.implicitNodes(role)), []) .filter(r => r !== null); -// Check if the current element is the skiplink -function isSkipLink(vNode) { - return skipLink && skipLink === vNode.actualNode; -} - // Check if the current element is a landmark function isRegion(virtualNode) { const node = virtualNode.actualNode; @@ -61,7 +43,8 @@ function findRegionlessElms(virtualNode) { // End recursion if the element is a landmark, skiplink, or hidden content if ( isRegion(virtualNode) || - isSkipLink(virtualNode) || + (dom.isSkipLink(virtualNode.actualNode) && + dom.getElementByReference(virtualNode.actualNode, 'href')) || !dom.isVisible(node, true) ) { return []; diff --git a/lib/commons/dom/is-skip-link.js b/lib/commons/dom/is-skip-link.js new file mode 100644 index 0000000000..e9fd5a100a --- /dev/null +++ b/lib/commons/dom/is-skip-link.js @@ -0,0 +1,37 @@ +/* global dom */ + +// test for hrefs that start with # or /# (for angular) +const isInternalLinkRegex = /^\/?#[^/!]/; + +/** + * Determines if element is a skip link + * @method isSkipLink + * @memberof axe.commons.dom + * @instance + * @param {Element} element + * @return {Boolean} + */ +dom.isSkipLink = function(element) { + if (!isInternalLinkRegex.test(element.getAttribute('href'))) { + return false; + } + + // define a skip link as any anchor element whose href starts with `#...` + // and which precedes the first anchor element whose href doesn't start + // with `#...` (that is, a link to a page) + const firstPageLink = axe.utils.querySelectorAll( + axe._tree, + 'a:not([href^="#"]):not([href^="/#"]):not([href^="javascript"])' + )[0]; + + // if there are no page links then all all links will need to be + // considered as skip links + if (!firstPageLink) { + return true; + } + + return ( + element.compareDocumentPosition(firstPageLink.actualNode) === + element.DOCUMENT_POSITION_FOLLOWING + ); +}; diff --git a/lib/rules/skip-link-matches.js b/lib/rules/skip-link-matches.js index 24bab16507..c642f0d443 100644 --- a/lib/rules/skip-link-matches.js +++ b/lib/rules/skip-link-matches.js @@ -1 +1 @@ -return /^#[^/!]/.test(node.getAttribute('href')); +return axe.commons.dom.isSkipLink(node); diff --git a/lib/rules/skip-link.json b/lib/rules/skip-link.json index c47905ea6f..7d7e5d71b4 100644 --- a/lib/rules/skip-link.json +++ b/lib/rules/skip-link.json @@ -1,6 +1,6 @@ { "id": "skip-link", - "selector": "a[href]", + "selector": "a[href^=\"#\"], a[href^=\"/#\"]", "matches": "skip-link-matches.js", "tags": ["cat.keyboard", "best-practice"], "metadata": { diff --git a/test/checks/navigation/skip-link.js b/test/checks/navigation/skip-link.js index d44f3ae71b..1b9eac0a27 100644 --- a/test/checks/navigation/skip-link.js +++ b/test/checks/navigation/skip-link.js @@ -7,33 +7,28 @@ describe('skip-link', function() { fixture.innerHTML = ''; }); - it('should return false if the href points to another document', function() { - fixture.innerHTML = - 'Click Here

Introduction

'; - var node = fixture.querySelector('a'); - assert.isFalse(checks['skip-link'].evaluate(node)); - }); - - it('should return false if the href points to a non-existent element', function() { - fixture.innerHTML = - 'Click Here

Introduction

'; - var node = fixture.querySelector('a'); - assert.isFalse(checks['skip-link'].evaluate(node)); - }); - it('should return true if the href points to an element with an ID', function() { fixture.innerHTML = 'Click Here

Introduction

'; + axe._tree = axe.utils.getFlattenedTree(fixture); var node = fixture.querySelector('a'); assert.isTrue(checks['skip-link'].evaluate(node)); }); it('should return true if the href points to an element with an name', function() { fixture.innerHTML = 'Click Here'; + axe._tree = axe.utils.getFlattenedTree(fixture); var node = fixture.querySelector('a'); assert.isTrue(checks['skip-link'].evaluate(node)); }); + it('should return false if the href points to a non-existent element', function() { + fixture.innerHTML = + 'Click Here

Introduction

'; + var node = fixture.querySelector('a'); + assert.isFalse(checks['skip-link'].evaluate(node)); + }); + it('should return undefined if the target has display:none', function() { fixture.innerHTML = 'Click Here' + @@ -49,18 +44,4 @@ describe('skip-link', function() { var node = fixture.querySelector('a'); assert.isUndefined(checks['skip-link'].evaluate(node)); }); - - it('should return true if the URI encoded href points to an element with an ID', function() { - fixture.innerHTML = - 'Click Here

Introduction

'; - var node = fixture.querySelector('a'); - assert.isTrue(checks['skip-link'].evaluate(node)); - }); - - it('should return true if the URI is an Angular skiplink', function() { - fixture.innerHTML = - 'Click Here

Introduction

'; - var node = fixture.querySelector('a'); - assert.isTrue(checks['skip-link'].evaluate(node)); - }); }); diff --git a/test/commons/dom/is-skip-link.js b/test/commons/dom/is-skip-link.js new file mode 100644 index 0000000000..e28bad9adb --- /dev/null +++ b/test/commons/dom/is-skip-link.js @@ -0,0 +1,71 @@ +describe('dom.isSkipLink', function() { + 'use strict'; + + var fixture = document.getElementById('fixture'); + + afterEach(function() { + fixture.innerHTML = ''; + }); + + it('should return true if the href points to an ID', function() { + fixture.innerHTML = 'Click Here'; + axe._tree = axe.utils.getFlattenedTree(fixture); + var node = fixture.querySelector('a'); + assert.isTrue(axe.commons.dom.isSkipLink(node)); + }); + + it('should return false if the href points to another document', function() { + fixture.innerHTML = 'Click Here'; + axe._tree = axe.utils.getFlattenedTree(fixture); + var node = fixture.querySelector('a'); + assert.isFalse(axe.commons.dom.isSkipLink(node)); + }); + + it('should return true if the URI encoded href points to an element with an ID', function() { + fixture.innerHTML = 'Click Here'; + axe._tree = axe.utils.getFlattenedTree(fixture); + var node = fixture.querySelector('a'); + assert.isTrue(axe.commons.dom.isSkipLink(node)); + }); + + it('should return true if the URI is an Angular skiplink', function() { + fixture.innerHTML = 'Click Here'; + axe._tree = axe.utils.getFlattenedTree(fixture); + var node = fixture.querySelector('a'); + assert.isTrue(axe.commons.dom.isSkipLink(node)); + }); + + it('should return true for multiple skip-links', function() { + fixture.innerHTML = + 'Click Here>Click Here>Click Here>'; + axe._tree = axe.utils.getFlattenedTree(fixture); + var nodes = fixture.querySelectorAll('a'); + for (var i = 0; i < nodes.length; i++) { + assert.isTrue(axe.commons.dom.isSkipLink(nodes[i])); + } + }); + + it('should return true if the element is before a page link', function() { + fixture.innerHTML = + 'New Page'; + axe._tree = axe.utils.getFlattenedTree(fixture); + var node = fixture.querySelector('#skip-link'); + assert.isTrue(axe.commons.dom.isSkipLink(node)); + }); + + it('should return false if the element is after a page link', function() { + fixture.innerHTML = + 'New Page'; + axe._tree = axe.utils.getFlattenedTree(fixture); + var node = fixture.querySelector('#skip-link'); + assert.isFalse(axe.commons.dom.isSkipLink(node)); + }); + + it('should ignore links that start with `href=javascript`', function() { + fixture.innerHTML = + 'New Page'; + axe._tree = axe.utils.getFlattenedTree(fixture); + var node = fixture.querySelector('#skip-link'); + assert.isTrue(axe.commons.dom.isSkipLink(node)); + }); +}); diff --git a/test/integration/full/skip-link/skip-link-fail.html b/test/integration/full/skip-link/skip-link-fail.html new file mode 100644 index 0000000000..c05b83d747 --- /dev/null +++ b/test/integration/full/skip-link/skip-link-fail.html @@ -0,0 +1,25 @@ + + + + skip-link test + + + + + + + + + + bad link 1 +
+ + + + \ No newline at end of file diff --git a/test/integration/full/skip-link/skip-link-fail.js b/test/integration/full/skip-link/skip-link-fail.js new file mode 100644 index 0000000000..57ffe7a8e1 --- /dev/null +++ b/test/integration/full/skip-link/skip-link-fail.js @@ -0,0 +1,41 @@ +describe('skip-link test pass', function() { + 'use strict'; + var results; + + before(function(done) { + axe.testUtils.awaitNestedLoad(function() { + axe.run({ runOnly: { type: 'rule', values: ['skip-link'] } }, function( + err, + r + ) { + assert.isNull(err); + results = r; + done(); + }); + }); + }); + + describe('violations', function() { + it('should find 1', function() { + assert.lengthOf(results.violations, 1); + }); + + it('should find 1 nodes', function() { + assert.lengthOf(results.violations[0].nodes, 1); + }); + }); + + describe('passes', function() { + it('should find 0', function() { + assert.lengthOf(results.passes, 0); + }); + }); + + it('should find 0 inapplicable', function() { + assert.lengthOf(results.inapplicable, 0); + }); + + it('should find 0 incomplete', function() { + assert.lengthOf(results.incomplete, 0); + }); +}); diff --git a/test/integration/full/skip-link/skip-link-pass.html b/test/integration/full/skip-link/skip-link-pass.html new file mode 100644 index 0000000000..dca2300bc4 --- /dev/null +++ b/test/integration/full/skip-link/skip-link-pass.html @@ -0,0 +1,42 @@ + + + + skip-link test + + + + + + + + + + +
+ Link + + Link + +
+ Link (angular) + + + Link + + + + + link + link +
+ + + + \ No newline at end of file diff --git a/test/integration/full/skip-link/skip-link-pass.js b/test/integration/full/skip-link/skip-link-pass.js new file mode 100644 index 0000000000..9db6232707 --- /dev/null +++ b/test/integration/full/skip-link/skip-link-pass.js @@ -0,0 +1,37 @@ +describe('skip-link test pass', function() { + 'use strict'; + var results; + + before(function(done) { + axe.testUtils.awaitNestedLoad(function() { + axe.run({ runOnly: { type: 'rule', values: ['skip-link'] } }, function( + err, + r + ) { + assert.isNull(err); + results = r; + done(); + }); + }); + }); + + describe('violations', function() { + it('should find 0', function() { + assert.lengthOf(results.violations, 0); + }); + }); + + describe('passes', function() { + it('should find 3', function() { + assert.lengthOf(results.passes[0].nodes, 3); + }); + }); + + it('should find 0 inapplicable', function() { + assert.lengthOf(results.inapplicable, 0); + }); + + it('should find 1 incomplete', function() { + assert.lengthOf(results.incomplete, 1); + }); +}); diff --git a/test/integration/rules/skip-link/skip-link.html b/test/integration/rules/skip-link/skip-link.html deleted file mode 100644 index 20abcba65f..0000000000 --- a/test/integration/rules/skip-link/skip-link.html +++ /dev/null @@ -1,13 +0,0 @@ -link -link - -
-Link - - -Link - - -Link - -bad link 1 diff --git a/test/integration/rules/skip-link/skip-link.json b/test/integration/rules/skip-link/skip-link.json deleted file mode 100644 index 302e81644d..0000000000 --- a/test/integration/rules/skip-link/skip-link.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rule": "skip-link", - "violations": [["#fail1"]], - "incomplete": [["#canttell1"]], - "passes": [["#pass1"], ["#pass2"]] -} diff --git a/test/rule-matches/skip-link-matches.js b/test/rule-matches/skip-link-matches.js index 2e38e2a1a5..b06ec749f5 100644 --- a/test/rule-matches/skip-link-matches.js +++ b/test/rule-matches/skip-link-matches.js @@ -9,6 +9,7 @@ describe('skip-link-matches', function() { return rule.id === 'skip-link'; }); link = document.createElement('a'); + fixture.innerHTML = '
'; }); afterEach(function() { @@ -29,11 +30,6 @@ describe('skip-link-matches', function() { assert.isFalse(rule.matches(link)); }); - it('returns true if the href attribute starts with #', function() { - link.href = '#foo'; - assert.isTrue(rule.matches(link)); - }); - it('returns false if the href attribute starts with #!', function() { link.href = '#!foo'; assert.isFalse(rule.matches(link)); @@ -43,4 +39,14 @@ describe('skip-link-matches', function() { link.href = '#/foo'; assert.isFalse(rule.matches(link)); }); + + it('returns true if the href attribute starts with #', function() { + link.href = '#main'; + assert.isTrue(rule.matches(link)); + }); + + it('returns true if the href attribute starts with /# (angular)', function() { + link.href = '/#main'; + assert.isTrue(rule.matches(link)); + }); });