Skip to content

Commit

Permalink
feat: new rule landmark-complementary-is-top-level (#1239)
Browse files Browse the repository at this point in the history
* feat: new rule landmark-complementary-is-top-level

Best practice requiring asides and complementary landmarks to be top level, in line with the ARIA Authoring Practices Guide.

Closes #795

* fix: remove matches from top-level aside rule

The first version of this rule included a body context check copied over from the top-level-banner-landmark rule, and it made very flaky iframe tests. It wasn't really necessary since aside doesn't have the same behavior as header/role=banner.

* test: improve readability of landmark check
  • Loading branch information
marcysutton authored Dec 4, 2018
1 parent 59465dc commit 328ca2c
Show file tree
Hide file tree
Showing 12 changed files with 344 additions and 121 deletions.
2 changes: 1 addition & 1 deletion doc/aria-supported.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ For a detailed description about how accessibility support is decided, see [How
| -------------------- | ---------------- |
| aria-describedat | No |
| aria-details | No |
| aria-roledescription | No |
| aria-roledescription | No |
153 changes: 77 additions & 76 deletions doc/rule-descriptions.md

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions lib/rules/landmark-complementary-is-top-level.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"id": "landmark-complementary-is-top-level",
"selector": "aside:not([role]), [role=complementary]",
"tags": [
"cat.semantics",
"best-practice"
],
"metadata": {
"description": "Ensures the complementary landmark or aside is at top level",
"help": "Aside must not be contained in another landmark"
},
"all": [],
"any": [
"landmark-is-top-level"
],
"none": []
}
2 changes: 1 addition & 1 deletion lib/rules/landmark-has-body-context.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const nativeScopeFilter = 'article, aside, main, nav, section';

// 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
// e.g. a <header> inside a <main> is not a banner, but in the <body> context it is
return (
node.hasAttribute('role') ||
!axe.commons.dom.findUpVirtual(virtualNode, nativeScopeFilter)
Expand Down
79 changes: 36 additions & 43 deletions test/checks/keyboard/landmark-is-top-level.js
Original file line number Diff line number Diff line change
@@ -1,79 +1,72 @@
describe('landmark-is-top-level', function() {
'use strict';

var fixture = document.getElementById('fixture');

var shadowSupported = axe.testUtils.shadowSupport.v1;
var checkSetup = axe.testUtils.checkSetup;
var shadowCheckSetup = axe.testUtils.shadowCheckSetup;
var check = checks['landmark-is-top-level'];
var checkContext = new axe.testUtils.MockCheckContext();

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

it('should return false if the landmark is in another landmark', function() {
var mainLandmark = document.createElement('main');
var bannerDiv = document.createElement('div');
bannerDiv.setAttribute('role', 'banner');
bannerDiv.appendChild(mainLandmark);
fixture.appendChild(bannerDiv);
assert.isFalse(check.evaluate.call(checkContext, mainLandmark));
it('should return false if the main landmark is in another landmark', function() {
var params = checkSetup(
'<div role="banner"><main id="target"></main></div>'
);
assert.isFalse(check.evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, { role: 'main' });
});

it('should return false if the complementary landmark is in another landmark', function() {
var params = checkSetup(
'<main><div role="complementary" id="target"></div></main>'
);
assert.isFalse(check.evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, { role: 'complementary' });
});

it('should return false if div with role set to main is in another landmark', function() {
var mainDiv = document.createElement('div');
mainDiv.setAttribute('role', 'main');
var navDiv = document.createElement('div');
navDiv.setAttribute('role', 'navigation');
navDiv.appendChild(mainDiv);
fixture.appendChild(navDiv);
assert.isFalse(check.evaluate.call(checkContext, mainDiv));
var params = checkSetup(
'<div role="navigation"><div role="main" id="target"></div></div>'
);
assert.isFalse(check.evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, { role: 'main' });
});

it('should return true if the landmark is not in another landmark', function() {
var footerLandmark = document.createElement('footer');
var bannerDiv = document.createElement('div');
bannerDiv.setAttribute('role', 'banner');
fixture.appendChild(bannerDiv);
fixture.appendChild(footerLandmark);
assert.isTrue(check.evaluate.call(checkContext, footerLandmark));
var params = checkSetup(
'<div><footer id="target"></footer><div role="banner"></div></div>'
);
assert.isTrue(check.evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, { role: 'contentinfo' });
});

it('should return true if div with role set to main is not in another landmark', function() {
var mainDiv = document.createElement('div');
mainDiv.setAttribute('role', 'main');
var navDiv = document.createElement('div');
navDiv.setAttribute('role', 'navigation');
fixture.appendChild(navDiv);
fixture.appendChild(mainDiv);
assert.isTrue(check.evaluate.call(checkContext, mainDiv));
var params = checkSetup(
'<div><div role="main" id="target"></div><div role="navigation"></div></div>'
);
assert.isTrue(check.evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, { role: 'main' });
});

it('should return true if the landmark is in form landmark', function() {
var bannerDiv = document.createElement('div');
bannerDiv.setAttribute('role', 'banner');
var formDiv = document.createElement('div');
formDiv.setAttribute('role', 'form');
fixture.appendChild(formDiv);
fixture.appendChild(bannerDiv);
assert.isTrue(check.evaluate.call(checkContext, bannerDiv));
it('should return true if the banner landmark is not in form landmark', function() {
var params = checkSetup(
'<div><div role="banner" id="target"></div><div role="form"></div></div>'
);
assert.isTrue(check.evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, { role: 'banner' });
});

(shadowSupported ? it : xit)(
'should test if the landmark in shadow DOM is top level',
function() {
var div = document.createElement('div');
var shadow = div.attachShadow({ mode: 'open' });
shadow.innerHTML = '<main>Main content</main>';
var checkArgs = checkSetup(shadow.querySelector('main'));
assert.isTrue(check.evaluate.apply(checkContext, checkArgs));
var params = shadowCheckSetup(
'<div></div>',
'<main id="target">Main content</main>'
);
assert.isTrue(check.evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, { role: 'main' });
}
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!doctype html>
<html id="violation2">
<head>
<meta charset="utf8">
<script src="/axe.js"></script>
</head>
<body>
<p>This iframe should fail, too</p>
<main>
<div role="complementary">
<p>This complementary landmark is in a main landmark</p>
</div>
</main>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!doctype html>
<html id="pass2">
<head>
<meta charset="utf8">
<script src="/axe.js"></script>
</head>
<body>
<p>This iframe should pass, too</p>

<div role="navigation">
<p>This div has role navigation</p>
</div>
<header>
<p>This banner content is not within another landmark</p>
</header>
<div role="complementary">
<p>This div has role complementary</p>
</div>
<div role="search">
<p>This div has role search</p>
</div>
<div role="form">
<p>This div has role form<p>
</div>
<iframe id="frame2" src="level2.html"></iframe>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html id="pass3">
<head>
<meta charset="utf8">
<script src="/axe.js"></script>
</head>
<body>
<p>This iframe should pass<p>
<aside>
<p>This aside is top level and should be ignored</p>
</aside>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!doctype html>
<html lang="en" id="violation1">
<head>
<title>landmark-complementary-is-top-level 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 src="/axe.js"></script>
<script>
mocha.setup({
timeout: 10000,
ui: 'bdd'
});
var assert = chai.assert;
</script>
</head>
<body>
<div role="navigation">
<div role="complementary">
<p>This is going to fail</p>
</div>
</div>
<iframe id="frame1" src="frames/level1-fail.html"></iframe>
<div id="mocha"></div>
<script src="landmark-complementary-is-top-level-fail.js"></script>
<script src="/test/integration/adapter.js"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
describe('landmark-complementary-is-top-level test fail', function() {
'use strict';
var results;
before(function(done) {
window.addEventListener('load', function() {
axe.run(
{
runOnly: {
type: 'rule',
values: ['landmark-complementary-is-top-level']
}
},
function(err, r) {
assert.isNull(err);
results = r;
done();
}
);
});
});

describe('violations', function() {
it('should find 1', function() {
assert.lengthOf(results.violations, 1);
});

it('should find 2 nodes', function() {
assert.lengthOf(results.violations[0].nodes, 2);
});
});

describe('passes', function() {
it('should find none', function() {
assert.lengthOf(results.passes, 0);
});
});

it('should find 0 inapplicable', function() {
assert.lengthOf(results.inapplicable, 0);
});

it('should find 0 incomplete', function() {
assert.lengthOf(results.incomplete, 0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!doctype html>
<html lang="en" id="pass1">
<head>
<title>landmark-complementary-is-top-level 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 src="/axe.js"></script>
<script>
mocha.setup({
timeout: 10000,
ui: 'bdd'
});
var assert = chai.assert;
</script>
</head>
<body>
<div role="navigation">
<p>This div has role navigation</p>
</div>
<div role="banner">
<p>This banner content is not within another landmark</p>
</div>
<aside>
<p>This aside has an implicit role complementary</p>
</aside>
<div role="complementary">
<p>This div has role complementary</p>
</div>
<div role="search">
<p>This div has role search</p>
</div>
<div role="form">
<p>This div has role form<p>
</div>
<iframe id="frame1" src="frames/level1.html"></iframe>
<div id="mocha"></div>
<script src="landmark-complementary-is-top-level-pass.js"></script>
<script src="/test/integration/adapter.js"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
describe('landmark-complementary-is-top-level test pass', function() {
'use strict';
var results;
before(function(done) {
window.addEventListener('load', function() {
axe.run(
{
runOnly: {
type: 'rule',
values: ['landmark-complementary-is-top-level']
}
},
function(err, r) {
assert.isNull(err);
results = r;
done();
}
);
});
});

describe('violations', function() {
it('should find 0', function() {
assert.lengthOf(results.violations, 0);
});
});

describe('passes', function() {
it('should find 4', function() {
assert.lengthOf(results.passes[0].nodes, 4);
});
});

it('should find 0 inapplicable', function() {
assert.lengthOf(results.inapplicable, 0);
});

it('should find 0 incomplete', function() {
assert.lengthOf(results.incomplete, 0);
});
});

0 comments on commit 328ca2c

Please sign in to comment.