Skip to content

Commit

Permalink
Merge pull request #760 from dequelabs/landmark-no-duplicate
Browse files Browse the repository at this point in the history
New rules: No duplicate header/footer
  • Loading branch information
WilcoFiers authored Mar 7, 2018
2 parents 5bbfb32 + 83c806d commit 3e2acb1
Show file tree
Hide file tree
Showing 25 changed files with 633 additions and 50 deletions.
2 changes: 2 additions & 0 deletions doc/rule-descriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
| label-title-only | Ensures that every form element is not solely labeled using the title or aria-describedby attributes | cat.forms, best-practice | true |
| label | Ensures every form element has a label | cat.forms, wcag2a, wcag332, wcag131, section508, section508.22.n | true |
| landmark-main-is-top-level | The main landmark should not be contained in another landmark | best-practice | true |
| landmark-no-duplicate-banner | Ensures the document has no more than one banner landmark | best-practice | true |
| landmark-no-duplicate-contentinfo | Ensures the document has no more than one contentinfo landmark | best-practice | true |
| landmark-one-main | Ensures a navigation point to the primary content of the page. If the page contains iframes, each iframe should contain either no main landmarks or just one | best-practice | true |
| layout-table | Ensures presentational <table> elements do not use <th>, <caption> elements or the summary attribute | cat.semantics, wcag2a, wcag131 | true |
| link-in-text-block | Links can be distinguished without relying on color | cat.color, experimental, wcag2a, wcag141 | true |
Expand Down
2 changes: 0 additions & 2 deletions lib/checks/keyboard/has-no-more-than-one-main.js

This file was deleted.

15 changes: 15 additions & 0 deletions lib/checks/keyboard/page-no-duplicate-banner.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"id": "page-no-duplicate-banner",
"evaluate": "page-no-duplicate.js",
"options": {
"selector": "header:not([role]), [role=banner]",
"nativeScopeFilter": "article, aside, main, nav, section"
},
"metadata": {
"impact": "moderate",
"messages": {
"pass": "Document has no more than one banner landmark",
"fail": "Document has more than one banner landmark"
}
}
}
15 changes: 15 additions & 0 deletions lib/checks/keyboard/page-no-duplicate-contentinfo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"id": "page-no-duplicate-contentinfo",
"evaluate": "page-no-duplicate.js",
"options": {
"selector": "footer:not([role]), [role=contentinfo]",
"nativeScopeFilter": "article, aside, main, nav, section"
},
"metadata": {
"impact": "moderate",
"messages": {
"pass": "Document has no more than one contentinfo landmark",
"fail": "Document has more than one contentinfo landmark"
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"id": "has-no-more-than-one-main",
"evaluate": "has-no-more-than-one-main.js",
"id": "page-no-duplicate-main",
"evaluate": "page-no-duplicate.js",
"options": {
"selector": "main:not([role]), [role='main']"
},
"metadata": {
"impact": "moderate",
"messages": {
Expand Down
18 changes: 18 additions & 0 deletions lib/checks/keyboard/page-no-duplicate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
if (!options || !options.selector || typeof options.selector !== 'string') {
throw new TypeError('visible-in-page requires options.selector to be a string');
}

let elms = axe.utils.querySelectorAll(virtualNode, options.selector);

// Filter elements that, within certain contexts, don't map their role.
// e.g. a <footer> inside a <main> is not a banner, but in the <body> context it is
if (typeof options.nativeScopeFilter === 'string') {
elms = elms.filter(elm => {
return (elm.actualNode.hasAttribute('role') ||
!axe.commons.dom.findUpVirtual(elm, options.nativeScopeFilter));
});
}

this.relatedNodes(elms.map(elm => elm.actualNode));

return elms.length <= 1;
16 changes: 16 additions & 0 deletions lib/rules/landmark-no-duplicate-banner.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"id": "landmark-no-duplicate-banner",
"selector": "html",
"tags": [
"best-practice"
],
"metadata": {
"description": "Ensures the document has no more than one banner landmark",
"help": "Document contain at most one banner landmark"
},
"all": [],
"any": [
"page-no-duplicate-banner"
],
"none": []
}
16 changes: 16 additions & 0 deletions lib/rules/landmark-no-duplicate-contentinfo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"id": "landmark-no-duplicate-contentinfo",
"selector": "html",
"tags": [
"best-practice"
],
"metadata": {
"description": "Ensures the document has no more than one contentinfo landmark",
"help": "Document contain at most one contentinfo landmark"
},
"all": [],
"any": [
"page-no-duplicate-contentinfo"
],
"none": []
}
2 changes: 1 addition & 1 deletion lib/rules/landmark-one-main.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"all": [
"page-has-main",
"has-no-more-than-one-main"
"page-no-duplicate-main"
],
"any": [],
"none": []
Expand Down
45 changes: 0 additions & 45 deletions test/checks/keyboard/has-no-more-than-one-main.js

This file was deleted.

103 changes: 103 additions & 0 deletions test/checks/keyboard/page-no-duplicate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
describe('page-no-duplicate', function () {
'use strict';

var fixture = document.getElementById('fixture');
var checkContext = new axe.testUtils.MockCheckContext();
var checkSetup = axe.testUtils.checkSetup;
var shadowSupported = axe.testUtils.shadowSupport.v1;

var check = checks['page-no-duplicate-main'];

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

describe('options.selector', function () {
it('throws if there is no selector', function () {
assert.throws(function () {
var params = checkSetup('<div id="target"></div>', undefined);
assert.isFalse(check.evaluate.apply(checkContext, params));
});
});

it('should return false if there is more than one element matching the selector', function () {
var options = { selector: 'main' };
var params = checkSetup('<div id="target"><main></main><main></main></div>', options);

assert.isFalse(check.evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._relatedNodes,
Array.from(fixture.querySelectorAll('main')));
});

it('should return true if there is only one element matching the selector', function(){
var options = { selector: 'main' };
var params = checkSetup('<div role="main" id="target"></div>', options);
assert.isTrue(check.evaluate.apply(checkContext, params));
});

it('should return true if there are no element matching the selector', function(){
var options = { selector: 'footer' };
var params = checkSetup('<div id="target"><main></main><main></main></div>', options);
assert.isTrue(check.evaluate.apply(checkContext, params));
});

(shadowSupported ? it : xit)
('should return false if there is a second matching element inside the shadow dom', function () {
var options = { selector: 'main' };
var div = document.createElement('div');
div.innerHTML = '<div id="shadow"></div><main></main>';

var shadow = div.querySelector('#shadow').attachShadow({ mode: 'open' });
shadow.innerHTML = '<main></main>';
axe.testUtils.fixtureSetup(div);

assert.isFalse(check.evaluate.call(checkContext, fixture, options, axe._tree[0]));
assert.deepEqual(checkContext._relatedNodes, [
shadow.querySelector('main'),
div.querySelector('main')
]);
});
});

describe('option.nativeScopeFilter', function () {
it('should ignore element contained in a nativeScopeFilter match', function () {
var options = {
selector: 'footer',
nativeScopeFilter: 'main'
};
var params = checkSetup('<div id="target"><footer></footer>' +
'<main><footer></footer></main>' +
'</div>', options);
assert.isTrue(check.evaluate.apply(checkContext, params));
});

it('should not ignore element contained in a nativeScopeFilter match with their roles redefined', function () {
var options = {
selector: 'footer, [role="contentinfo"]',
nativeScopeFilter: 'main'
};
var params = checkSetup('<div id="target"><footer></footer>' +
'<main><div role="contentinfo"></div></main>' +
'</div>', options);
assert.isFalse(check.evaluate.apply(checkContext, params));
});

(shadowSupported ? it : xit)
('elements if its ancestor is outside the shadow DOM tree', function () {
var options = {
selector: 'footer',
nativeScopeFilter: 'main'
};

var div = document.createElement('div');
div.innerHTML = '<main id="shadow"></main><footer></footer>';
div.querySelector('#shadow')
.attachShadow({ mode: 'open' })
.innerHTML = '<footer></footer>';
axe.testUtils.fixtureSetup(div);

assert.isTrue(check.evaluate.call(checkContext, fixture, options, axe._tree[0]));
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en" id="fail2">
<head>
<meta charset="utf8">
<script src="/axe.js"></script>
</head>
<body>
<header>
Header 1
</header>
<header>
Header 2
</header>
<iframe id="frame2" src="level2.html"></iframe>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en" id="pass2">
<head>
<meta charset="utf8">
<script src="/axe.js"></script>
</head>
<body>
<header>
Top level header
</header>
<article>
<header>
Header in article
</header>
</article>
<aside>
<header>
Header in aside
</header>
</aside>
<main>
<header>
Header in main landmark
</header>
</main>
<nav>
<header>
Header in nav
</header>
</nav>
<section>
<header>
Header in section
</header>
</section>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en" id="fail3">
<head>
<meta charset="utf8">
<script src="/axe.js"></script>
</head>
<body>
<main>
<div role="banner">
Div 1 with role banner in main landmark
</div>
<div role="banner">
Div 2 with role banner in main landmark
</div>
</main>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!doctype html>
<html lang="en" id="fail1">
<head>
<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 src="/axe.js"></script>
<script>
mocha.setup({
timeout: 10000,
ui: 'bdd'
});
var assert = chai.assert;
</script>
</head>
<body>
<div role="banner">
Div 1 with role of "banner"
</div>
<div role="banner">
Div 2 with role of "banner"
</div>
<iframe id="frame1" src="frames/level1-fail.html"></iframe>
<div id="mocha"></div>
<script src="landmark-no-duplicate-banner-fail.js"></script>
<script src="/test/integration/adapter.js"></script>
</body>
</html>
Loading

0 comments on commit 3e2acb1

Please sign in to comment.