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): Adding landmark-is-unique rule #1394

Merged
merged 45 commits into from
Jun 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
4a8beea
Merge pull request #1 from dequelabs/develop
waabid Apr 4, 2018
c906f06
Merge branch 'develop' of https://github.com/dequelabs/axe-core into …
waabid Feb 21, 2019
b6d1986
Initial attempt at landmark-unique rule
markreay Dec 13, 2018
d490bf0
Adding matches tests WIP
waabid Feb 21, 2019
560b9f8
Re-factored and added tests for matches function
waabid Feb 26, 2019
6fe36e7
Merge branch 'develop' of https://github.com/dequelabs/axe-core into …
waabid Feb 27, 2019
016466b
Added support for virtual nodes
waabid Feb 27, 2019
3fe93c3
Using findUp api instead of looking up the tree ourselves
waabid Feb 27, 2019
6cebced
Migrated unique-ness logic to after and removed code
waabid Feb 28, 2019
06900ad
Added tests for landmark-is-unique-after
waabid Feb 28, 2019
bfce046
Added best practice tag to unique landmark rule
waabid Feb 28, 2019
e29a2fc
Added test for form elements
waabid Feb 28, 2019
0ff7e4c
Merge branch 'develop' into users/waabid/uniqueLandmarks2
dylanb Mar 1, 2019
92a9afe
Added comment to explain filtering in landmark matches function
waabid Mar 1, 2019
8a9dcb0
Merge branch 'users/waabid/uniqueLandmarks2' of https://github.com/wa…
waabid Mar 1, 2019
20fd686
Changed usage of actual node with virtual node where possible in land…
waabid Mar 1, 2019
da1a124
Use related nodes api instead of failing duplicate landmarks
waabid Mar 1, 2019
7e6c652
Updated landmark is unique check test to validate related nodes
waabid Mar 1, 2019
de967e0
Added a failing iframe test for landmark-unique
waabid Mar 1, 2019
94dc1f1
Added a test case for matching landmark-unique against elements in sh…
waabid Mar 2, 2019
536366d
Implement own findIndex for landmark-is-unique to unblock tests
waabid Mar 2, 2019
0dec669
Used find instead of findIndex (from polyfills)
waabid Mar 5, 2019
ef8a005
Updated the landmark-unique-matches to use the virtual node by default
waabid Mar 5, 2019
526b48c
Updated nodeName usage to upper case
waabid Mar 5, 2019
87956ac
Exit early when no role is found in landmark unique matches
waabid Mar 5, 2019
9197736
Added test for heirarchical exclusions in shadow dom for landmark-uni…
waabid Mar 6, 2019
b4878db
Took out common variables in landmark unique matches tests
waabid Mar 6, 2019
6ef1760
Added more iframe integration tests for landmark-unique rule
waabid Mar 6, 2019
0b97ebe
Merge branch 'develop' into users/waabid/uniqueLandmarks2
waabid Mar 6, 2019
72c2fb8
Merge https://github.com/dequelabs/axe-core into develop
waabid Mar 29, 2019
13fb04f
Merge branch 'develop' into users/waabid/uniqueLandmarks2
waabid Mar 29, 2019
932bb7a
PR changes: label to accessibleText, ES3 support in tests, and remove…
waabid Apr 4, 2019
203800c
Added more integration test cases
waabid Apr 5, 2019
efa4d52
Updated text for new rule
waabid Apr 5, 2019
a45253f
One last const to var
waabid Apr 5, 2019
acd6720
Merge https://github.com/dequelabs/axe-core into users/waabid/uniqueL…
waabid Apr 5, 2019
ea8f641
Updated to remove all const/let uses
waabid Apr 5, 2019
4dfc817
Use accessibletextvirtual
waabid Apr 5, 2019
dd8b5cc
Fixed test by passing virtual node in to landmark is unique
waabid Apr 18, 2019
cc0a4b9
Merge https://github.com/dequelabs/axe-core into users/waabid/uniqueL…
waabid Apr 18, 2019
85d3240
Merge remote-tracking branch 'upstream/develop' into users/waabid/uni…
straker May 23, 2019
9e1f8ec
sync upstream/develop
straker May 23, 2019
3b3580a
remove es6 syntax from tests
straker May 23, 2019
aafa4a1
cleanup
straker Jun 4, 2019
e64d406
fix typo
straker Jun 4, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/rule-descriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
| landmark-no-duplicate-banner | Ensures the document has at most one banner landmark | Moderate | cat.semantics, best-practice | true |
| landmark-no-duplicate-contentinfo | Ensures the document has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | true |
| landmark-one-main | Ensures the document has only one main landmark and each iframe in the page has at most one main landmark | Moderate | cat.semantics, best-practice | true |
| landmark-unique | Landmarks must have a unique role or role/label/title (i.e. accessible name) combination | Moderate | cat.semantics, best-practice | true |
| layout-table | Ensures presentational <table> elements do not use <th>, <caption> elements or the summary attribute | Serious | cat.semantics, wcag2a, wcag131 | true |
| link-in-text-block | Links can be distinguished without relying on color | Serious | cat.color, experimental, wcag2a, wcag141 | true |
| link-name | Ensures links have discernible text | Serious | cat.name-role-value, wcag2a, wcag412, wcag244, section508, section508.22.a | true |
Expand Down
23 changes: 23 additions & 0 deletions lib/checks/landmarks/landmark-is-unique-after.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
var uniqueLandmarks = [];

// filter out landmark elements that share the same role and accessible text
// so every non-unique landmark isn't reported as a failure (just the first)
return results.filter(currentResult => {
waabid marked this conversation as resolved.
Show resolved Hide resolved
straker marked this conversation as resolved.
Show resolved Hide resolved
var findMatch = someResult => {
return (
currentResult.data.role === someResult.data.role &&
currentResult.data.accessibleText === someResult.data.accessibleText
);
};

var matchedResult = uniqueLandmarks.find(findMatch);
if (matchedResult) {
matchedResult.result = false;
matchedResult.relatedNodes.push(currentResult.relatedNodes[0]);
return false;
}

uniqueLandmarks.push(currentResult);
currentResult.relatedNodes = [];
return true;
});
7 changes: 7 additions & 0 deletions lib/checks/landmarks/landmark-is-unique.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
var role = axe.commons.aria.getRole(node);
var accessibleText = axe.commons.text.accessibleTextVirtual(virtualNode);
accessibleText = accessibleText ? accessibleText.toLowerCase() : null;
this.data({ role: role, accessibleText: accessibleText });
this.relatedNodes([node]);

return true;
12 changes: 12 additions & 0 deletions lib/checks/landmarks/landmark-is-unique.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"id": "landmark-is-unique",
"evaluate": "landmark-is-unique.js",
"after": "landmark-is-unique-after.js",
"metadata": {
"impact": "moderate",
"messages": {
"pass": "Landmarks must have a unique role or role/label/title (i.e. accessible name) combination",
"fail": "The landmark must have a unique aria-label, aria-labelledby, or title to make landmarks distinguishable"
}
}
}
41 changes: 41 additions & 0 deletions lib/rules/landmark-unique-matches.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Since this is a best-practice rule, we are filtering elements as dictated by ARIA 1.1 Practices regardless of treatment by browser/AT combinations.
*
* Info: https://www.w3.org/TR/wai-aria-practices-1.1/#aria_landmark
*/
var excludedParentsForHeaderFooterLandmarks = [
'article',
'aside',
'main',
'nav',
'section'
].join(',');
function isHeaderFooterLandmark(headerFooterElement) {
return !axe.commons.dom.findUpVirtual(
headerFooterElement,
excludedParentsForHeaderFooterLandmarks
);
}

function isLandmarkVirtual(virtualNode) {
var { actualNode } = virtualNode;
var landmarkRoles = axe.commons.aria.getRolesByType('landmark');
var role = axe.commons.aria.getRole(actualNode);
if (!role) {
return false;
}

var nodeName = actualNode.nodeName.toUpperCase();
if (nodeName === 'HEADER' || nodeName === 'FOOTER') {
return isHeaderFooterLandmark(virtualNode);
}

if (nodeName === 'SECTION' || nodeName === 'FORM') {
waabid marked this conversation as resolved.
Show resolved Hide resolved
var accessibleText = axe.commons.text.accessibleTextVirtual(virtualNode);
return !!accessibleText;
}

waabid marked this conversation as resolved.
Show resolved Hide resolved
return landmarkRoles.indexOf(role) >= 0 || role === 'region';
}

return isLandmarkVirtual(virtualNode) && axe.commons.dom.isVisible(node, true);
13 changes: 13 additions & 0 deletions lib/rules/landmark-unique.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"id": "landmark-unique",
"selector": "[role=banner], [role=complementary], [role=contentinfo], [role=main], [role=navigation], [role=region], [role=search], [role=form], form, footer, header, aside, main, nav, section",
"tags": ["cat.semantics", "best-practice"],
"metadata": {
"help": "Ensures landmarks are unique",
"description": "Landmarks must have a unique role or role/label/title (i.e. accessible name) combination"
},
"matches": "landmark-unique-matches.js",
"all": [],
"any": ["landmark-is-unique"],
"none": []
}
82 changes: 82 additions & 0 deletions test/checks/landmarks/landmark-is-unique-after.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
describe('landmark-is-unique-after', function() {
'use strict';

var checkContext = axe.testUtils.MockCheckContext();
function createResult(result, data) {
return {
result: result,
data: data
};
}

function createResultWithSameRelatedNodes(result, data) {
return Object.assign(createResult(result, data), {
relatedNodes: [createResult(result, data)]
});
}

function createResultWithProvidedRelatedNodes(result, data, relatedNodes) {
return Object.assign(createResult(result, data), {
relatedNodes: relatedNodes
});
}

afterEach(function() {
axe._tree = undefined;
checkContext.reset();
});

it('should update duplicate landmarks with failed result', function() {
var result = checks['landmark-is-unique'].after([
createResultWithSameRelatedNodes(true, {
role: 'some role',
accessibleText: 'some accessibleText'
}),
createResultWithSameRelatedNodes(true, {
role: 'some role',
accessibleText: 'some accessibleText'
}),
createResultWithSameRelatedNodes(true, {
role: 'different role',
accessibleText: 'some accessibleText'
}),
createResultWithSameRelatedNodes(true, {
role: 'some role',
accessibleText: 'different accessibleText'
})
]);

var expectedResult = [
createResultWithProvidedRelatedNodes(
false,
{
role: 'some role',
accessibleText: 'some accessibleText'
},
[
createResult(true, {
role: 'some role',
accessibleText: 'some accessibleText'
})
]
),
createResultWithProvidedRelatedNodes(
true,
{
role: 'different role',
accessibleText: 'some accessibleText'
},
[]
),
createResultWithProvidedRelatedNodes(
true,
{
role: 'some role',
accessibleText: 'different accessibleText'
},
[]
)
];
assert.deepEqual(result, expectedResult);
});
});
59 changes: 59 additions & 0 deletions test/checks/landmarks/landmark-is-unique.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
describe('landmark-is-unique', function() {
'use strict';

var checkContext = new axe.testUtils.MockCheckContext();
var fixture;
var axeFixtureSetup;

beforeEach(function() {
fixture = document.getElementById('fixture');
axeFixtureSetup = axe.testUtils.fixtureSetup;
});

afterEach(function() {
axe._tree = undefined;
checkContext.reset();
});

it('should return true, with correct role and no accessible text', function() {
axeFixtureSetup('<div role="main">test</div>');
var node = fixture.querySelector('div');
var expectedData = {
accessibleText: null,
role: 'main'
};
axe._tree = axe.utils.getFlattenedTree(fixture);
var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node);
assert.isTrue(
checks['landmark-is-unique'].evaluate.call(
checkContext,
node,
{},
virtualNode
)
);
assert.deepEqual(checkContext._data, expectedData);
assert.deepEqual(checkContext._relatedNodes, [node]);
});

it('should return true, with correct role and the accessible text lowercased', function() {
axeFixtureSetup('<div role="main" aria-label="TEST text">test</div>');
var node = fixture.querySelector('div');
var expectedData = {
accessibleText: 'test text',
role: 'main'
};
axe._tree = axe.utils.getFlattenedTree(fixture);
var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node);
assert.isTrue(
checks['landmark-is-unique'].evaluate.call(
checkContext,
node,
{},
virtualNode
)
);
assert.deepEqual(checkContext._data, expectedData);
assert.deepEqual(checkContext._relatedNodes, [node]);
});
});
14 changes: 14 additions & 0 deletions test/integration/rules/landmark-unique/frame.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf8" />
<title>landmark-unique test</title>
<script src="/axe.js"></script>
</head>
<body>
<main id="violation-main-2">Second main</main>
dylanb marked this conversation as resolved.
Show resolved Hide resolved
<div id="form-label-3">iframe-form-with-label</div>
<div role="form" aria-labelledby="form-label-3"></div>
<div role="navigation"></div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<main id="violation-main-1">First main</main>
waabid marked this conversation as resolved.
Show resolved Hide resolved
<iframe src="landmark-unique/frame.html" title="iframe with main" id="frame"></iframe>

<header id="violation-header-1">First header</header>
<header id="violation-header-2">Second header</header>

<form id="violation-form-aria-label-1" aria-label="form-label"></form>
<form id="violation-form-aria-label-2" aria-label="form-label"></form>

<div id="form-label-1">form-with-label</div>
<div id="form-label-2">form-with-label</div>
<form id="violation-form-aria-labelledby-1" aria-labelledby="form-label-1"></form>
<form id="violation-form-aria-labelledby-2" aria-labelledby="form-label-2"></form>

<form id="violation-aside-aria-label-1" aria-label="aside-label"></form>
<form id="violation-aside-aria-label-2" aria-label="aside-label"></form>

<div id="aside-label-1">aside-with-label</div>
<div id="aside-label-2">aside-with-label</div>
<form id="violation-aside-aria-labelledby-1" aria-labelledby="aside-label-1"></form>
<form id="violation-aside-aria-labelledby-2" aria-labelledby="aside-label-2"></form>

<footer id="violation-footer-1">First footer</footer>
<footer id="violation-footer-2">Second footer</footer>

<div id="form-label-3">iframe-form-with-label</div>
<div id="violation-form-through-iframe-1" role="form" aria-labelledby="form-label-3"></div>

<div id="violation-nav-through-iframe-1" role="navigation"></div>

<div id="violation-role-banner" aria-label="duplicate label" role="banner"></div>
<div id="violation-role-banner-2" aria-label="duplicate label" role="banner"></div>

<div id="violation-role-complementary" role="complementary"></div>
<div id="violation-role-complementary-2" role="complementary"></div>

<div id="violation-role-contentinfo" aria-label="duplicate label for contentinfo" role="contentinfo"></div>
<div id="violation-role-contentinfo-2" aria-label="duplicate label for contentinfo" role="contentinfo"></div>

<div id="violation-role-main" aria-label="duplicate label for main" role="main"></div>
<div id="violation-role-main-2" aria-label="duplicate label for main" role="main"></div>

<div id="violation-role-region" role="region"></div>
<div id="violation-role-region-2" role="region"></div>

<div id="violation-role-search" role="search"></div>
<div id="violation-role-search-2" role="search"></div>

<nav id="violation-nav" aria-label="duplicate label for nav"></nav>
<nav id="violation-nav-2" aria-label="duplicate label for nav"></nav>

<section id="violation-section" aria-label="duplicate label for section"></section>
<section id="violation-section-2" aria-label="duplicate label for section"></section>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"description": "landmark-unique-fail tests",
"rule": "landmark-unique",
"violations": [
["#violation-main-1"],
["#violation-header-1"],
["#violation-form-aria-label-1"],
["#violation-form-aria-labelledby-1"],
["#violation-aside-aria-label-1"],
["#violation-aside-aria-labelledby-1"],
["#violation-footer-1"],
["#violation-form-through-iframe-1"],
["#violation-nav-through-iframe-1"],
["#violation-role-banner"],
["#violation-role-complementary"],
["#violation-role-contentinfo"],
["#violation-role-main"],
["#violation-role-region"],
["#violation-role-search"],
["#violation-nav"],
["#violation-section"]
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<main id="pass-main">Only main</main>

<header id="pass-header">Only header</header>

<form id="pass-form-aria-label-1" aria-label="form-label-1"></form>
<form id="pass-form-aria-label-2" aria-label="form-label-2"></form>

<div id="form-label-1">form-with-label-1</div>
<div id="form-label-2">form-with-label-2</div>
<form id="pass-form-aria-labelledby-1" aria-labelledby="form-label-1"></form>
<form id="pass-form-aria-labelledby-2" aria-labelledby="form-label-2"></form>

<form id="pass-aside-aria-label-1" aria-label="aside-label-1"></form>
<form id="pass-aside-aria-label-2" aria-label="aside-label-2"></form>

<div id="aside-label-1">aside-with-label-1</div>
<div id="aside-label-2">aside-with-label-2</div>
<form id="pass-aside-aria-labelledby-1" aria-labelledby="aside-label-1"></form>
<form id="pass-aside-aria-labelledby-2" aria-labelledby="aside-label-2"></form>

<footer id="pass-footer">Only footer</footer>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"description": "landmark-unique-pass tests",
"rule": "landmark-unique",
"passes": [
["#pass-main"],
["#pass-header"],
["#pass-form-aria-label-1"],
["#pass-form-aria-label-2"],
["#pass-form-aria-labelledby-1"],
["#pass-form-aria-labelledby-2"],
["#pass-aside-aria-label-1"],
["#pass-aside-aria-label-2"],
["#pass-aside-aria-labelledby-1"],
["#pass-aside-aria-labelledby-2"],
["#pass-footer"]
]
}
Loading