From f007b471d6ab29ef37128295d72d25c76a2b86f7 Mon Sep 17 00:00:00 2001 From: Sulaiman Sanaullah Date: Tue, 14 Nov 2017 06:51:18 -0600 Subject: [PATCH] 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 --- doc/rule-descriptions.md | 1 + lib/checks/keyboard/main-is-top-level.js | 13 ++++ lib/checks/keyboard/main-is-top-level.json | 11 +++ lib/rules/landmark-main-is-top-level.json | 16 +++++ test/checks/keyboard/main-is-top-level.js | 70 +++++++++++++++++++ .../frames/level1-fail.html | 17 +++++ .../frames/level1.html | 29 ++++++++ .../frames/level2-a.html | 15 ++++ .../frames/level2.html | 15 ++++ .../landmark-main-is-top-level-fail.html | 29 ++++++++ .../landmark-main-is-top-level-fail.js | 42 +++++++++++ .../landmark-main-is-top-level-pass.html | 42 +++++++++++ .../landmark-main-is-top-level-pass.js | 36 ++++++++++ 13 files changed, 336 insertions(+) create mode 100644 lib/checks/keyboard/main-is-top-level.js create mode 100644 lib/checks/keyboard/main-is-top-level.json create mode 100644 lib/rules/landmark-main-is-top-level.json create mode 100644 test/checks/keyboard/main-is-top-level.js create mode 100644 test/integration/full/landmark-main-is-top-level/frames/level1-fail.html create mode 100644 test/integration/full/landmark-main-is-top-level/frames/level1.html create mode 100644 test/integration/full/landmark-main-is-top-level/frames/level2-a.html create mode 100644 test/integration/full/landmark-main-is-top-level/frames/level2.html create mode 100644 test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-fail.html create mode 100644 test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-fail.js create mode 100644 test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-pass.html create mode 100644 test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-pass.js diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 044af174d7..9525e12313 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -33,6 +33,7 @@ | input-image-alt | Ensures <input type="image"> elements have alternate text | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a | true | | label-title-only | Ensures that every form element is not solely labeled using the title or aria-describedby attributes | cat.forms, best-practice | false | | 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 | | 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 | | link-name | Ensures links have discernible text | cat.name-role-value, wcag2a, wcag111, wcag412, wcag244, section508, section508.22.a | true | diff --git a/lib/checks/keyboard/main-is-top-level.js b/lib/checks/keyboard/main-is-top-level.js new file mode 100644 index 0000000000..562c2d360f --- /dev/null +++ b/lib/checks/keyboard/main-is-top-level.js @@ -0,0 +1,13 @@ +var landmarks = axe.commons.aria.getRolesByType('landmark'); +var parent = axe.commons.dom.getComposedParent(node); +while (parent){ + var role = parent.getAttribute('role'); + if (!role && (parent.tagName.toLowerCase() !== 'form')){ + role = axe.commons.aria.implicitRole(parent); + } + if (role && landmarks.includes(role)){ + return false; + } + parent = axe.commons.dom.getComposedParent(parent); +} +return true; diff --git a/lib/checks/keyboard/main-is-top-level.json b/lib/checks/keyboard/main-is-top-level.json new file mode 100644 index 0000000000..1f0038d336 --- /dev/null +++ b/lib/checks/keyboard/main-is-top-level.json @@ -0,0 +1,11 @@ +{ + "id": "main-is-top-level", + "evaluate": "main-is-top-level.js", + "metadata": { + "impact": "moderate", + "messages": { + "pass": "The main landmark is at the top level.", + "fail": "The main landmark is contained in another landmark." + } + } +} diff --git a/lib/rules/landmark-main-is-top-level.json b/lib/rules/landmark-main-is-top-level.json new file mode 100644 index 0000000000..b4255d4200 --- /dev/null +++ b/lib/rules/landmark-main-is-top-level.json @@ -0,0 +1,16 @@ +{ + "id": "landmark-main-is-top-level", + "selector": "main,[role=main]", + "tags": [ + "best-practice" + ], + "metadata": { + "description": "The main landmark should not be contained in another landmark", + "help": "Main landmark is not at top level" + }, + "all": [], + "any": [ + "main-is-top-level" + ], + "none": [] +} diff --git a/test/checks/keyboard/main-is-top-level.js b/test/checks/keyboard/main-is-top-level.js new file mode 100644 index 0000000000..27316deaa0 --- /dev/null +++ b/test/checks/keyboard/main-is-top-level.js @@ -0,0 +1,70 @@ +describe('main-is-top-level', function () { + 'use strict'; + + var fixture = document.getElementById('fixture'); + + var shadowSupported = axe.testUtils.shadowSupport.v1; + var checkSetup = axe.testUtils.checkSetup; + + afterEach(function () { + fixture.innerHTML = ''; + }); + + it('should return false if main 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(checks['main-is-top-level'].evaluate(mainLandmark)); + }); + + 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(checks['main-is-top-level'].evaluate(mainDiv)); + }); + + it('should return true if main landmark is not in another landmark', function () { + var mainLandmark = document.createElement('main'); + var bannerDiv = document.createElement('div'); + bannerDiv.setAttribute('role','banner'); + fixture.appendChild(bannerDiv); + fixture.appendChild(mainLandmark); + assert.isTrue(checks['main-is-top-level'].evaluate(mainLandmark)); + }); + + 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(checks['main-is-top-level'].evaluate(mainDiv)); + }); + + it('should return true if main is in form landmark', function () { + var mainDiv = document.createElement('div'); + mainDiv.setAttribute('role','main'); + var formDiv = document.createElement('div'); + formDiv.setAttribute('role','form'); + fixture.appendChild(formDiv); + fixture.appendChild(mainDiv); + assert.isTrue(checks['main-is-top-level'].evaluate(mainDiv)); + }); + + + (shadowSupported ? it : xit)('should test if main in shadow DOM is top level', function () { + var div = document.createElement('div'); + var shadow = div.attachShadow({ mode: 'open' }); + shadow.innerHTML = '
Main content
'; + var checkArgs = checkSetup(shadow.querySelector('main')); + assert.isTrue(checks['main-is-top-level'].evaluate.apply(null, checkArgs)); + }); + +}); diff --git a/test/integration/full/landmark-main-is-top-level/frames/level1-fail.html b/test/integration/full/landmark-main-is-top-level/frames/level1-fail.html new file mode 100644 index 0000000000..5edfede430 --- /dev/null +++ b/test/integration/full/landmark-main-is-top-level/frames/level1-fail.html @@ -0,0 +1,17 @@ + + + + + + + +

This iframe should fail, too

+
+
+

This main landmark is in a complementary landmark

+
+
+ + + + diff --git a/test/integration/full/landmark-main-is-top-level/frames/level1.html b/test/integration/full/landmark-main-is-top-level/frames/level1.html new file mode 100644 index 0000000000..5b143cbd9b --- /dev/null +++ b/test/integration/full/landmark-main-is-top-level/frames/level1.html @@ -0,0 +1,29 @@ + + + + + + + +

This iframe should pass, too

+ +
+

This div has role banner

+
+
+

This div has role navigation

+
+
+

This main content is not within another landmark

+
+
+

This div has role complementary

+
+
+

This div has role search

+
+
+

This div has role form

+

+ + diff --git a/test/integration/full/landmark-main-is-top-level/frames/level2-a.html b/test/integration/full/landmark-main-is-top-level/frames/level2-a.html new file mode 100644 index 0000000000..ac5566dd15 --- /dev/null +++ b/test/integration/full/landmark-main-is-top-level/frames/level2-a.html @@ -0,0 +1,15 @@ + + + + + + + +

This iframe is also a violation

+
+
+

This main landmark is in a navigation landmark

+
+
+ + diff --git a/test/integration/full/landmark-main-is-top-level/frames/level2.html b/test/integration/full/landmark-main-is-top-level/frames/level2.html new file mode 100644 index 0000000000..a0c30fd536 --- /dev/null +++ b/test/integration/full/landmark-main-is-top-level/frames/level2.html @@ -0,0 +1,15 @@ + + + + + + + +

This iframe is another violation

+

+
+

This main landmark is in a search landmark

+
+
+ + diff --git a/test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-fail.html b/test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-fail.html new file mode 100644 index 0000000000..a52fc576f3 --- /dev/null +++ b/test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-fail.html @@ -0,0 +1,29 @@ + + + + landmark-main-is-top-level test + + + + + + + + +
+
+

This is going to fail

+
+
+ +
+ + + + \ No newline at end of file diff --git a/test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-fail.js b/test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-fail.js new file mode 100644 index 0000000000..2531fd2515 --- /dev/null +++ b/test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-fail.js @@ -0,0 +1,42 @@ + +describe('landmark-main-is-top-level test fail', function () { + 'use strict'; + var results; + before(function (done) { + window.addEventListener('load', function () { + axe.run({ runOnly: { type: 'rule', values: ['landmark-main-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 4 nodes', function () { + assert.lengthOf(results.violations[0].nodes, 4); + }); + }); + + 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); + }); + +}); + diff --git a/test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-pass.html b/test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-pass.html new file mode 100644 index 0000000000..d54481c6e6 --- /dev/null +++ b/test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-pass.html @@ -0,0 +1,42 @@ + + + + landmark-main-is-top-level test + + + + + + + + +
+

This div has role banner

+
+
+

This div has role navigation

+
+
+

This main content is not within another landmark

+
+
+

This div has role complementary

+
+
+

This div has role search

+
+
+

This div has role form

+

+ +
+ + + + \ No newline at end of file diff --git a/test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-pass.js b/test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-pass.js new file mode 100644 index 0000000000..7ee26da173 --- /dev/null +++ b/test/integration/full/landmark-main-is-top-level/landmark-main-is-top-level-pass.js @@ -0,0 +1,36 @@ + +describe('landmark-main-is-top-level test pass', function () { + 'use strict'; + var results; + before(function (done) { + window.addEventListener('load', function () { + axe.run({ runOnly: { type: 'rule', values: ['landmark-main-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 2', function () { + assert.lengthOf(results.passes[0].nodes, 2); + }); + }); + + it('should find 0 inapplicable', function () { + assert.lengthOf(results.inapplicable, 0); + }); + + it('should find 0 incomplete', function () { + assert.lengthOf(results.incomplete, 0); + }); + +}); +