Skip to content

Commit 63040bd

Browse files
sulsanaulWilcoFiers
authored andcommitted
feat: Add rule, landmark-main-is-top-level (#462)
* init * feat: add new rule, landmark-no-more-than-one-main * test(landmark-at-least-one-main): updated integration tests for check * feat(landmark-main-is-top-level): add rule ensuring each main landmark in a document is not within another landmark element * refactor(landmark-main-is-top-level): change help messages * feat(landmark-main-is-top-level): change a function used in check and update aria index file so application type is structure * refactor(main-is-top-level): change pass/fail messages * refactor(landmark-main-is-top-level): change description/help messages * feat(main-is-top-level): update check for shadow dom features * fix(main-is-top-level): update check to ignore form as a landmark * fix: edit incorrect usage of getComposedParent * test: add unit test to check if main landmark in shadow dom is top level * style: remove spaces in attributes * fix: update test so that checkSetup passes in correct argument * test: add test to ensure correct number of violations nodes * fix: revert 'application' type to landmark * style: remove spaces in attributes * fix: allow main landmark to be in form
1 parent 63e1272 commit 63040bd

13 files changed

+336
-0
lines changed

doc/rule-descriptions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
| input-image-alt | Ensures <input type="image"> elements have alternate text | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a | true |
3434
| label-title-only | Ensures that every form element is not solely labeled using the title or aria-describedby attributes | cat.forms, best-practice | false |
3535
| label | Ensures every form element has a label | cat.forms, wcag2a, wcag332, wcag131, section508, section508.22.n | true |
36+
| landmark-main-is-top-level | The main landmark should not be contained in another landmark | best-practice | true |
3637
| layout-table | Ensures presentational <table> elements do not use <th>, <caption> elements or the summary attribute | cat.semantics, wcag2a, wcag131 | true |
3738
| link-in-text-block | Links can be distinguished without relying on color | cat.color, experimental, wcag2a, wcag141 | true |
3839
| link-name | Ensures links have discernible text | cat.name-role-value, wcag2a, wcag111, wcag412, wcag244, section508, section508.22.a | true |
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
var landmarks = axe.commons.aria.getRolesByType('landmark');
2+
var parent = axe.commons.dom.getComposedParent(node);
3+
while (parent){
4+
var role = parent.getAttribute('role');
5+
if (!role && (parent.tagName.toLowerCase() !== 'form')){
6+
role = axe.commons.aria.implicitRole(parent);
7+
}
8+
if (role && landmarks.includes(role)){
9+
return false;
10+
}
11+
parent = axe.commons.dom.getComposedParent(parent);
12+
}
13+
return true;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"id": "main-is-top-level",
3+
"evaluate": "main-is-top-level.js",
4+
"metadata": {
5+
"impact": "moderate",
6+
"messages": {
7+
"pass": "The main landmark is at the top level.",
8+
"fail": "The main landmark is contained in another landmark."
9+
}
10+
}
11+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"id": "landmark-main-is-top-level",
3+
"selector": "main,[role=main]",
4+
"tags": [
5+
"best-practice"
6+
],
7+
"metadata": {
8+
"description": "The main landmark should not be contained in another landmark",
9+
"help": "Main landmark is not at top level"
10+
},
11+
"all": [],
12+
"any": [
13+
"main-is-top-level"
14+
],
15+
"none": []
16+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
describe('main-is-top-level', function () {
2+
'use strict';
3+
4+
var fixture = document.getElementById('fixture');
5+
6+
var shadowSupported = axe.testUtils.shadowSupport.v1;
7+
var checkSetup = axe.testUtils.checkSetup;
8+
9+
afterEach(function () {
10+
fixture.innerHTML = '';
11+
});
12+
13+
it('should return false if main landmark is in another landmark', function () {
14+
var mainLandmark = document.createElement('main');
15+
var bannerDiv = document.createElement('div');
16+
bannerDiv.setAttribute('role','banner');
17+
bannerDiv.appendChild(mainLandmark);
18+
fixture.appendChild(bannerDiv);
19+
assert.isFalse(checks['main-is-top-level'].evaluate(mainLandmark));
20+
});
21+
22+
it('should return false if div with role set to main is in another landmark', function () {
23+
var mainDiv = document.createElement('div');
24+
mainDiv.setAttribute('role','main');
25+
var navDiv = document.createElement('div');
26+
navDiv.setAttribute('role','navigation');
27+
navDiv.appendChild(mainDiv);
28+
fixture.appendChild(navDiv);
29+
assert.isFalse(checks['main-is-top-level'].evaluate(mainDiv));
30+
});
31+
32+
it('should return true if main landmark is not in another landmark', function () {
33+
var mainLandmark = document.createElement('main');
34+
var bannerDiv = document.createElement('div');
35+
bannerDiv.setAttribute('role','banner');
36+
fixture.appendChild(bannerDiv);
37+
fixture.appendChild(mainLandmark);
38+
assert.isTrue(checks['main-is-top-level'].evaluate(mainLandmark));
39+
});
40+
41+
it('should return true if div with role set to main is not in another landmark', function () {
42+
var mainDiv = document.createElement('div');
43+
mainDiv.setAttribute('role','main');
44+
var navDiv = document.createElement('div');
45+
navDiv.setAttribute('role','navigation');
46+
fixture.appendChild(navDiv);
47+
fixture.appendChild(mainDiv);
48+
assert.isTrue(checks['main-is-top-level'].evaluate(mainDiv));
49+
});
50+
51+
it('should return true if main is in form landmark', function () {
52+
var mainDiv = document.createElement('div');
53+
mainDiv.setAttribute('role','main');
54+
var formDiv = document.createElement('div');
55+
formDiv.setAttribute('role','form');
56+
fixture.appendChild(formDiv);
57+
fixture.appendChild(mainDiv);
58+
assert.isTrue(checks['main-is-top-level'].evaluate(mainDiv));
59+
});
60+
61+
62+
(shadowSupported ? it : xit)('should test if main in shadow DOM is top level', function () {
63+
var div = document.createElement('div');
64+
var shadow = div.attachShadow({ mode: 'open' });
65+
shadow.innerHTML = '<main>Main content</main>';
66+
var checkArgs = checkSetup(shadow.querySelector('main'));
67+
assert.isTrue(checks['main-is-top-level'].evaluate.apply(null, checkArgs));
68+
});
69+
70+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!doctype html>
2+
<html id="violation2">
3+
<head>
4+
<meta charset="utf8">
5+
<script src="/axe.js"></script>
6+
</head>
7+
<body>
8+
<p>This iframe should fail, too</p>
9+
<div role="complementary">
10+
<div role="main">
11+
<p>This main landmark is in a complementary landmark</p>
12+
</div>
13+
</div>
14+
<iframe id="frame2" src="level2.html"></iframe>
15+
<iframe id="frame3" src="level2-a.html"></iframe>
16+
</body>
17+
</html>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!doctype html>
2+
<html id="pass2">
3+
<head>
4+
<meta charset="utf8">
5+
<script src="/axe.js"></script>
6+
</head>
7+
<body>
8+
<p>This iframe should pass, too</p>
9+
10+
<div role="banner">
11+
<p>This div has role banner</p>
12+
</div>
13+
<div role="navigation">
14+
<p>This div has role navigation</p>
15+
</div>
16+
<div role="main">
17+
<p>This main content is not within another landmark</p>
18+
</div>
19+
<div role="complementary">
20+
<p>This div has role complementary</p>
21+
</div>
22+
<div role="search">
23+
<p>This div has role search</p>
24+
</div>
25+
<div role="form">
26+
<p>This div has role form<p>
27+
</div>
28+
</body>
29+
</html>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!doctype html>
2+
<html id="violation4">
3+
<head>
4+
<meta charset="utf8">
5+
<script src="/axe.js"></script>
6+
</head>
7+
<body>
8+
<p>This iframe is also a violation</p>
9+
<div role="navigation">
10+
<main>
11+
<p>This main landmark is in a navigation landmark</p>
12+
</main>
13+
</div>
14+
</body>
15+
</html>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!doctype html>
2+
<html id="violation3">
3+
<head>
4+
<meta charset="utf8">
5+
<script src="/axe.js"></script>
6+
</head>
7+
<body>
8+
<p>This iframe is another violation<p>
9+
<div role="search">
10+
<main>
11+
<p>This main landmark is in a search landmark</p>
12+
</main>
13+
</div>
14+
</body>
15+
</html>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!doctype html>
2+
<html lang="en" id="violation1">
3+
<head>
4+
<title>landmark-main-is-top-level test</title>
5+
<meta charset="utf8">
6+
<link rel="stylesheet" type="text/css" href="/node_modules/mocha/mocha.css" />
7+
<script src="/node_modules/mocha/mocha.js"></script>
8+
<script src="/node_modules/chai/chai.js"></script>
9+
<script src="/axe.js"></script>
10+
<script>
11+
mocha.setup({
12+
timeout: 10000,
13+
ui: 'bdd'
14+
});
15+
var assert = chai.assert;
16+
</script>
17+
</head>
18+
<body>
19+
<div role="navigation">
20+
<div role="main">
21+
<p>This is going to fail</p>
22+
</div>
23+
</div>
24+
<iframe id="frame1" src="frames/level1-fail.html"></iframe>
25+
<div id="mocha"></div>
26+
<script src="landmark-main-is-top-level-fail.js"></script>
27+
<script src="/test/integration/adapter.js"></script>
28+
</body>
29+
</html>

0 commit comments

Comments
 (0)