Skip to content

Commit

Permalink
feat(rule): Adding landmark-is-unique rule (dequelabs#1394)
Browse files Browse the repository at this point in the history
* Initial attempt at landmark-unique rule

* Adding matches tests WIP

* Re-factored and added tests for matches function

* Added support for virtual nodes

* Using findUp api instead of looking up the tree ourselves

* Migrated unique-ness logic to after and removed code

* Added tests for landmark-is-unique-after

* Added best practice tag to unique landmark rule

* Added test for form elements

* Added comment to explain filtering in landmark matches function

* Changed usage of actual node with virtual node where possible in landmark-unique-matches

* Use related nodes api instead of failing duplicate landmarks

* Updated landmark is unique check test to validate related nodes

* Added a failing iframe test for landmark-unique

* Added a test case for matching landmark-unique against elements in shadow dom

* Implement own findIndex for landmark-is-unique to unblock tests

* Used find instead of findIndex (from polyfills)

* Updated the landmark-unique-matches to use the virtual node by default

* Updated nodeName usage to upper case

* Exit early when no role is found in landmark unique matches

* Added test for heirarchical exclusions in shadow dom for landmark-unique-matches

* Took out common variables in landmark unique matches tests

* Added more iframe integration tests for landmark-unique rule

* PR changes: label to accessibleText, ES3 support in tests, and remove options since not used

* Added more integration test cases

* Updated text for new rule

* One last const to var

* Updated to remove all const/let uses

* Use accessibletextvirtual

* Fixed test by passing virtual node in to landmark is unique

* sync upstream/develop

* remove es6 syntax from tests

* cleanup

* fix typo
  • Loading branch information
waabid authored and straker committed Jun 5, 2019
1 parent 753ecf4 commit 0088e94
Show file tree
Hide file tree
Showing 14 changed files with 567 additions and 0 deletions.
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 => {
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') {
var accessibleText = axe.commons.text.accessibleTextVirtual(virtualNode);
return !!accessibleText;
}

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>
<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>
53 changes: 53 additions & 0 deletions test/integration/rules/landmark-unique/landmark-unique-fail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<main id="violation-main-1">First main</main>
<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>
23 changes: 23 additions & 0 deletions test/integration/rules/landmark-unique/landmark-unique-fail.json
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"]
]
}
21 changes: 21 additions & 0 deletions test/integration/rules/landmark-unique/landmark-unique-pass.html
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>
17 changes: 17 additions & 0 deletions test/integration/rules/landmark-unique/landmark-unique-pass.json
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

0 comments on commit 0088e94

Please sign in to comment.