Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rule): identical-links-same-purpose #1649

Merged
merged 60 commits into from Jan 15, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
0e74359
initial commit, files generated
jeeyyy Jun 5, 2019
56a79a6
add tests for rule matches
jeeyyy Jun 18, 2019
7a9f5e5
reconcile results in after fn
jeeyyy Jun 20, 2019
b4d85a8
add integration tests
jeeyyy Jun 21, 2019
41e0cbf
update tags and messages
jeeyyy Jun 21, 2019
54a58e1
update rule desc
jeeyyy Jun 21, 2019
b6313b9
some updates based on review
jeeyyy Jun 24, 2019
2ecfbb2
update
jeeyyy Jul 2, 2019
f365c13
update test
jeeyyy Jul 2, 2019
ddece99
chore: merge from develop
jeeyyy Aug 6, 2019
a580de9
fix: update rule implementation
jeeyyy Aug 13, 2019
7f45dcd
Merge branch 'develop' into rule-identical-links-same-purpose
jeeyyy Aug 13, 2019
67e1dfa
docs: update rule descriptions
jeeyyy Aug 13, 2019
d1ec366
fix: edge case when identical name do not have resource
jeeyyy Aug 15, 2019
2efa44e
test: update integration tests and add inapplicable tests
jeeyyy Aug 23, 2019
13222ed
fix: update matches, check and the tests
jeeyyy Aug 23, 2019
05b8d3a
update impl
jeeyyy Aug 26, 2019
6b69bcd
test: shadowDOM tests
jeeyyy Aug 26, 2019
4311d78
test: add after tests
jeeyyy Aug 26, 2019
6771316
refactor: helper methods that can be extracted to commons namespace l…
jeeyyy Aug 26, 2019
004026f
docs: update function comments
jeeyyy Aug 26, 2019
bff93fc
update
jeeyyy Aug 27, 2019
5953137
update
jeeyyy Aug 27, 2019
33a321d
update
jeeyyy Aug 27, 2019
48072b3
fix: update isVisible to handle AREA element
jeeyyy Sep 13, 2019
ac17884
fix: move getParsedResource to commons.dom namespace
jeeyyy Sep 13, 2019
425392a
fix: update rule and tests
jeeyyy Sep 13, 2019
1253bec
test: skip in phantomJs
jeeyyy Sep 13, 2019
0fcda3c
test: add nested iframe tests
jeeyyy Sep 13, 2019
9a562dc
test: update isvisible test
jeeyyy Sep 13, 2019
ebcfeaf
test: update rule matches tests
jeeyyy Sep 13, 2019
14dade1
chore: merge from develop
jeeyyy Sep 13, 2019
0498d1d
test: skip area tests in phantomjs
jeeyyy Sep 13, 2019
be24a4e
fix: tests
jeeyyy Sep 16, 2019
b3bab39
test: remove isPhantomJs skip for tests
jeeyyy Sep 16, 2019
9459f58
update fn urlPropsFromAttribute
jeeyyy Oct 28, 2019
5f2084a
update fn isAreaVisible
jeeyyy Oct 28, 2019
c1a6bb7
update integration tests
jeeyyy Oct 28, 2019
c11d793
update fn urlPropsFromAttribute
jeeyyy Oct 28, 2019
c959e5e
update after fn and fix tests
jeeyyy Oct 28, 2019
efeddf3
remove shadowDOM test in check
jeeyyy Oct 28, 2019
535f849
update port resolution from url to exclude defaults
jeeyyy Oct 28, 2019
882b3d5
chore: merge from develop
jeeyyy Jan 8, 2020
42dc874
update isVisible computation
jeeyyy Jan 9, 2020
f1276c3
add tests for isVisible
jeeyyy Jan 9, 2020
f0e1207
update urlPropsFromAttribute
jeeyyy Jan 9, 2020
8eb6a65
update tests
jeeyyy Jan 9, 2020
f59736a
update test
jeeyyy Jan 9, 2020
07d2168
chore: merge from develop
jeeyyy Jan 9, 2020
471cbc7
update assertion
jeeyyy Jan 9, 2020
40b9671
chore: merge from develop
jeeyyy Jan 10, 2020
4da8e0b
use findUp instead of closest
jeeyyy Jan 10, 2020
e3c33a2
updates based on review
jeeyyy Jan 14, 2020
c8a0667
remove experimental tag
jeeyyy Jan 14, 2020
e7a3e61
chore: merge from develop
jeeyyy Jan 14, 2020
dece1ef
ignore FTP url in IE11
jeeyyy Jan 14, 2020
4d65d51
update after fn to handle link results with no data
jeeyyy Jan 14, 2020
ef9337c
skip FTP testt on IE11
jeeyyy Jan 14, 2020
0f01e9d
empty commit to trigger build
jeeyyy Jan 14, 2020
34a4a84
changes based on review
jeeyyy Jan 15, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/rule-descriptions.md
Expand Up @@ -39,6 +39,7 @@
| html-has-lang | Ensures every HTML document has a lang attribute | Serious | cat.language, wcag2a, wcag311 | true |
| html-lang-valid | Ensures the lang attribute of the <html> element has a valid value | Serious | cat.language, wcag2a, wcag311 | true |
| html-xml-lang-mismatch | Ensure that HTML elements with both valid lang and xml:lang attributes agree on the base language of the page | Moderate | cat.language, wcag2a, wcag311 | true |
| identical-links-same-purpose | Ensures that links with identical accessible names resolve to the same resource | Minor | wcag2aaa, wcag249, experimental | true |
| image-alt | Ensures <img> elements have alternate text or a role of none or presentation | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a | true |
| image-redundant-alt | Ensure button and link text is not repeated as image alternative | Minor | cat.text-alternatives, best-practice | true |
| input-image-alt | Ensures <input type="image"> elements have alternate text | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a | true |
Expand Down
50 changes: 50 additions & 0 deletions lib/checks/navigation/identical-links-same-purpose-after.js
@@ -0,0 +1,50 @@
/**
* Skip unless there are more than a single result
*/
if (results.length < 2) {
return results;
}

/**
* for each result
* - get other results with matching accessible name
* - check if same purpose is served
* - if not change `result` to `undefined`
*/
for (let index = 0; index < results.length; index++) {
const { accessibleText, linkResource } = results[index].data;
const identicalLinks = getIdenticalLinks(accessibleText, index);

if (!hasSamePurpose(linkResource, identicalLinks)) {
results[index].result = undefined;
}
}

return results;
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved

/**
* Get list of items from results which match a prescribed accessible name
* @param {String} expectedAccessibleText accessible name to be matched
* @param {Number} excludeIndex exclude `index` of result, that should not be taken into consideration
* @returns {Array<Object>}
*/
function getIdenticalLinks(expectedAccessibleText, excludeIndex) {
return results.filter(
({ data: { accessibleText } }, index) =>
index !== excludeIndex && accessibleText === expectedAccessibleText
);
}

/**
* Check if a given set of links have same resource
* @param {String} expectedResource resource string
* @param {Array<Object>} identicalLinks results where `data.accessibleText` were identical
* @returns {Boolean}
*/
function hasSamePurpose(expectedResource, identicalLinks) {
return identicalLinks.every(({ data: { linkResource } }) => {
const expected = expectedResource.replace(/\s/g, '').toLowerCase();
const actual = linkResource.replace(/\s/g, '').toLowerCase();
return expected === actual;
});
}
48 changes: 48 additions & 0 deletions lib/checks/navigation/identical-links-same-purpose.js
@@ -0,0 +1,48 @@
/**
* Note:
* `identical-links-same-purpose-after` fn, helps reconcile the results & alter the CheckResult accordingly
*/
const { text } = axe.commons;
const accessibleText = text.accessibleTextVirtual(virtualNode);
const linkResource = getLinkResource(node);

if (!linkResource) {
return undefined;
}

/**
* Set `data` for use in `after` fn
*/
this.data({
accessibleText,
linkResource
});

return true;

/**
* Get resource that a element points to (eg: href or location redirects)
* @param {HTMLElement} el Element
* @returns {String|null}
*/
function getLinkResource(el) {
if (el.hasAttribute('href')) {
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
return el.getAttribute('href');
}

const resourceAttr = Array.from(axe.utils.getNodeAttributes(node)).find(
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
({ value }) => {
/**
* get any attribute which has `location.` or `location=` in its value
*/
const attrValue = value.replace(/\s/g, '');
return /^location[\.\=]/.test(attrValue);
}
);

if (resourceAttr) {
return resourceAttr.value;
}

return null;
}
12 changes: 12 additions & 0 deletions lib/checks/navigation/identical-links-same-purpose.json
@@ -0,0 +1,12 @@
{
"id": "identical-links-same-purpose",
"evaluate": "identical-links-same-purpose.js",
"after": "identical-links-same-purpose-after.js",
"metadata": {
"impact": "minor",
"messages": {
"pass": "Identical links (if any) resolve to same purpose",
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
"incomplete": "Fix purpose of links with same accessible name"
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
6 changes: 6 additions & 0 deletions lib/rules/identical-links-same-purpose-matches.js
@@ -0,0 +1,6 @@
const { aria, text } = axe.commons;

const role = aria.getRole(node);
const accText = text.accessibleTextVirtual(virtualNode);

return role === `link` && !!accText;
13 changes: 13 additions & 0 deletions lib/rules/identical-links-same-purpose.json
@@ -0,0 +1,13 @@
{
"id": "identical-links-same-purpose",
"selector": "a, area, [role=link]",
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
"matches": "identical-links-same-purpose-matches.js",
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
"tags": ["wcag2aaa", "wcag249", "experimental"],
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
"metadata": {
"description": "Ensures that links with identical accessible names resolve to the same resource",
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
"help": "Links with resolve to same or equivalent resources"
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
},
"all": ["identical-links-same-purpose"],
"any": [],
"none": []
}
118 changes: 118 additions & 0 deletions test/checks/navigation/identical-links-same-purpose.js
@@ -0,0 +1,118 @@
describe('identical-links-same-purpose tests', function() {
'use strict';

var fixture = document.getElementById('fixture');
var queryFixture = axe.testUtils.queryFixture;
var check = checks['identical-links-same-purpose'];
var checkContext = axe.testUtils.MockCheckContext();
var options = {};

afterEach(function() {
fixture.innerHTML = '';
checkContext.reset();
});

it('returns undefined when element does not have a resource (empty href)', function() {
var vNode = queryFixture('<a id="target" href="">Go to google.com</a>');
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
var actual = check.evaluate.call(
checkContext,
vNode.actualNode,
options,
vNode
);
assert.isUndefined(actual);
assert.isNull(checkContext._data);
});

it('returns undefined when element does not have a resource (onclick does not change location)', function() {
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
var vNode = queryFixture(
'<span id="target" role="link" tabindex="0" onclick="return false;">Link text</span>'
);
var actual = check.evaluate.call(
checkContext,
vNode.actualNode,
options,
vNode
);
assert.isUndefined(actual);
assert.isNull(checkContext._data);
});

it('returns true when element has location resource', function() {
var vNode = queryFixture(
'<a id="target" href="http://facebook.com">Follow us</a>'
);
var actual = check.evaluate.call(
checkContext,
vNode.actualNode,
options,
vNode
);
assert.isTrue(actual);
assert.hasAllKeys(checkContext._data, ['accessibleText', 'linkResource']);
});

it('returns true when element has location resource', function() {
var vNode = queryFixture(
'<span id="target" role="link" tabindex="0" onclick="location=\'/pages/index.html\'">Link text</span>'
);
var actual = check.evaluate.call(
checkContext,
vNode.actualNode,
options,
vNode
);
assert.isTrue(actual);
assert.hasAllKeys(checkContext._data, ['accessibleText', 'linkResource']);
});

describe('after', function() {
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
it('sets results of check result to `undefined` if links do not serve identical purpose', function() {
var checkResults = [
{
data: {
accessibleText: 'follow us',
linkResource: 'http://facebook.com'
},
result: true
},
{
data: {
accessibleText: 'follow us',
linkResource: 'http://instagram.com'
},
result: true
}
];
var results = check.after(checkResults);

assert.lengthOf(results, 2);
assert.isUndefined(results[0].result);
assert.isUndefined(results[1].result);
});

it('sets results of check result to `true` if links serve identical purpose', function() {
var checkResults = [
{
data: {
accessibleText: 'follow us',
linkResource: 'http://instagram.com/axe'
},
result: true
},
{
data: {
accessibleText: 'follow us',
linkResource: 'http://instagram.com/axe'
},
result: true
}
];
var results = check.after(checkResults);

assert.lengthOf(results, 2);
assert.isTrue(results[0].result);
assert.isTrue(results[1].result);
});
});
});
39 changes: 39 additions & 0 deletions test/integration/full/identical-links-same-purpose/incomplete.html
@@ -0,0 +1,39 @@
<!DOCTYPE html>
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved
<html lang="en">
<head>
<title>identical-links-same-purpose test</title>
<meta charset="utf8" />
<link
rel="stylesheet"
type="text/css"
href="/node_modules/mocha/mocha.css"
/>
<script src="/node_modules/mocha/mocha.js"></script>
<script src="/node_modules/chai/chai.js"></script>
<script>
mocha.setup({
timeout: 50000,
ui: 'bdd'
});
var assert = chai.assert;
</script>
<script src="/axe.js"></script>
</head>

<body>
<!-- Hiding mocha div so that the links generated by mocha stats do not intervene -->
<!-- <div id="mocha"></div> -->

<!-- Links #1 -->
<a href="/about/contact.html">Contact</a>
<a href="/admissions/contact.html">Contact</a>

<!-- Links #2 -->
<a href="http://facebook.com"><img src="facebook.jpg" alt="Follow us"/></a>
<a href="http://twitter.com"><img src="twitter.jpg" alt="Follow us"/></a>

<script src="/test/testutils.js"></script>
<script src="incomplete.js"></script>
<script src="/test/integration/adapter.js"></script>
</body>
</html>
26 changes: 26 additions & 0 deletions test/integration/full/identical-links-same-purpose/incomplete.js
@@ -0,0 +1,26 @@
describe('identical-links-same-purpose incomplete test', function() {
'use strict';

var runConfig = {
runOnly: {
type: 'rule',
values: ['identical-links-same-purpose']
}
};

it('returns incomplete results and no passes', function(done) {
axe.run(runConfig, function(err, res) {
assert.isNull(err);
assert.isDefined(res);

assert.lengthOf(res.passes, 0);
assert.lengthOf(res.incomplete, 1);
assert.lengthOf(res.incomplete[0].nodes, 4);
assert.equal(
res.incomplete[0].nodes[0].html,
'<a href="/about/contact.html">Contact</a>'
);
done();
});
});
});
55 changes: 55 additions & 0 deletions test/integration/full/identical-links-same-purpose/passes.html
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>identical-links-same-purpose test</title>
<meta charset="utf8" />
<link
rel="stylesheet"
type="text/css"
href="/node_modules/mocha/mocha.css"
/>
<script src="/node_modules/mocha/mocha.js"></script>
<script src="/node_modules/chai/chai.js"></script>
<script>
mocha.setup({
timeout: 50000,
ui: 'bdd'
});
var assert = chai.assert;
</script>
<script src="/axe.js"></script>
</head>

<body>
<!-- Hiding mocha div so that the links generated by mocha stats do not intervene -->
<div id="mocha"></div>
jeeyyy marked this conversation as resolved.
Show resolved Hide resolved

<!-- Identical Links #1 -->
<a href="/about/contact.html">Contact</a>
<a href="/about/contact.html">Contact</a>

<!-- Identical Links #2 -->
<a href="/company/index.html#contact">Contact Us</a>
<a href="/company/index.html#contact">Contact Us</a>

<!-- Identical Links #3 -->
<span
role="link"
tabindex="0"
onclick="location='https://www.holland.com/global/tourism.htm'"
>
Visit Netherlands
</span>
<span
role="link"
tabindex="0"
onclick="location='https://www.holland.com/global/tourism.htm'"
>
Visit Netherlands
</span>

<script src="/test/testutils.js"></script>
<script src="passes.js"></script>
<script src="/test/integration/adapter.js"></script>
</body>
</html>