From b40a3f5a7123c6aa6698481212c475ee7ca08ce8 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Thu, 15 Apr 2021 11:15:19 -0600 Subject: [PATCH 01/13] fix(skip-link): work with absoulte and relative paths --- lib/commons/dom/is-skip-link.js | 36 +++++++++++++------- test/commons/dom/is-skip-link.js | 56 ++++++++++++++++++++++++++++++++ test/playground.html | 20 ++++++++++++ 3 files changed, 100 insertions(+), 12 deletions(-) diff --git a/lib/commons/dom/is-skip-link.js b/lib/commons/dom/is-skip-link.js index 236b45155a..31301c7b7f 100644 --- a/lib/commons/dom/is-skip-link.js +++ b/lib/commons/dom/is-skip-link.js @@ -1,8 +1,11 @@ import cache from '../../core/base/cache'; import { querySelectorAll } from '../../core/utils'; -// test for hrefs that start with # or /# (for angular) -const isInternalLinkRegex = /^\/?#[^/!]/; +// angular skip links start with /# +const angularSkipLinkRegex = /^\/\#/; + +// angular router link uses #! or #/ +const angularRouterLinkRegex = /^#[!/]/; /** * Determines if element is a skip link @@ -13,22 +16,31 @@ const isInternalLinkRegex = /^\/?#[^/!]/; * @return {Boolean} */ function isSkipLink(element) { - if (!isInternalLinkRegex.test(element.getAttribute('href'))) { - return false; - } - let firstPageLink; if (typeof cache.get('firstPageLink') !== 'undefined') { firstPageLink = cache.get('firstPageLink'); } else { - // 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) + // define a skip link as any anchor element whose resolved href + // resolves to the current page and uses a fragment identifier (#) + // and which precedes the first anchor element whose resolved href + // does not resolve to the current page or that doesn't use a + // fragment identifier + const currentPage = window.location.origin + window.location.pathname; firstPageLink = querySelectorAll( - // TODO: es-module-_tree axe._tree, - 'a:not([href^="#"]):not([href^="/#"]):not([href^="javascript"])' - )[0]; + 'a[href]:not([href^="javascript"])' + ).find(({ actualNode }) => { + if (angularSkipLinkRegex.test(actualNode.getAttribute('href'))) { + return false; + } + + if (!actualNode.hash || angularRouterLinkRegex.test(actualNode.hash)) { + return true; + } + + const linkPage = actualNode.origin + actualNode.pathname; + return linkPage !== currentPage; + }); // null will signify no first page link cache.set('firstPageLink', firstPageLink || null); diff --git a/test/commons/dom/is-skip-link.js b/test/commons/dom/is-skip-link.js index 7e8a926c92..608c5ae3ba 100644 --- a/test/commons/dom/is-skip-link.js +++ b/test/commons/dom/is-skip-link.js @@ -2,9 +2,14 @@ describe('dom.isSkipLink', function() { 'use strict'; var fixture = document.getElementById('fixture'); + var baseEl; afterEach(function() { fixture.innerHTML = ''; + + if (baseEl) { + baseEl.remove(); + } }); it('should return true if the href points to an ID', function() { @@ -35,6 +40,20 @@ describe('dom.isSkipLink', function() { assert.isTrue(axe.commons.dom.isSkipLink(node)); }); + it('should return false if the URI is angular #!', 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 false if the URI is angular #/', 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 for multiple skip-links', function() { fixture.innerHTML = 'Click Here>Click Here>Click Here>'; @@ -68,4 +87,41 @@ describe('dom.isSkipLink', function() { var node = fixture.querySelector('#skip-link'); assert.isTrue(axe.commons.dom.isSkipLink(node)); }); + + it('should return true for hash href that resolves to current page', 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 absolute path hash href', function() { + var url = window.location.href; + 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 for absolute path href that points to another document', function() { + var origin = window.location.origin; + 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 false for href with tag that points to another document', function() { + baseEl = document.createElement('base'); + baseEl.href = 'https://www.google.com/'; + document.getElementsByTagName('head')[0].appendChild(baseEl); + + fixture.innerHTML = + 'Click Here'; + axe._tree = axe.utils.getFlattenedTree(fixture); + var node = fixture.querySelector('a'); + assert.isFalse(axe.commons.dom.isSkipLink(node)); + }); }); diff --git a/test/playground.html b/test/playground.html index 4de25a4c17..8a9ca52bef 100644 --- a/test/playground.html +++ b/test/playground.html @@ -1,8 +1,28 @@ O hai + + +
foo
+ hello + world + fobar + + pagelink + +
+ +
Hello
+ +
World
+ +
Fobar