From cb8f261ea58d60cc21b36812086eed3465a45f05 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Mon, 5 Mar 2018 18:07:51 +0100 Subject: [PATCH] feat(page-has-heading-one): Added new best-practice rule --- doc/rule-descriptions.md | 3 +- lib/checks/keyboard/page-has-elm-after.js | 8 +-- lib/checks/keyboard/page-has-elm.js | 8 +-- lib/checks/keyboard/page-has-heading-one.json | 15 +++++ lib/rules/landmark-one-main.json | 6 +- lib/rules/page-has-heading-one.json | 16 +++++ test/checks/keyboard/page-has-elm.js | 40 ++++++++++++- .../frames/level1-fail.html | 10 ++++ .../page-has-heading-one/frames/level1.html | 12 ++++ .../page-has-heading-one/frames/level2-a.html | 10 ++++ .../page-has-heading-one/frames/level2.html | 10 ++++ .../page-has-heading-one-fail.html | 24 ++++++++ .../page-has-heading-one-fail.js | 49 +++++++++++++++ .../page-has-heading-one-pass1.html | 24 ++++++++ .../page-has-heading-one-pass1.js | 59 +++++++++++++++++++ .../page-has-heading-one-pass2.html | 23 ++++++++ .../page-has-heading-one-pass2.js | 46 +++++++++++++++ 17 files changed, 349 insertions(+), 14 deletions(-) create mode 100644 lib/checks/keyboard/page-has-heading-one.json create mode 100644 lib/rules/page-has-heading-one.json create mode 100644 test/integration/full/page-has-heading-one/frames/level1-fail.html create mode 100644 test/integration/full/page-has-heading-one/frames/level1.html create mode 100644 test/integration/full/page-has-heading-one/frames/level2-a.html create mode 100644 test/integration/full/page-has-heading-one/frames/level2.html create mode 100644 test/integration/full/page-has-heading-one/page-has-heading-one-fail.html create mode 100644 test/integration/full/page-has-heading-one/page-has-heading-one-fail.js create mode 100644 test/integration/full/page-has-heading-one/page-has-heading-one-pass1.html create mode 100644 test/integration/full/page-has-heading-one/page-has-heading-one-pass1.js create mode 100644 test/integration/full/page-has-heading-one/page-has-heading-one-pass2.html create mode 100644 test/integration/full/page-has-heading-one/page-has-heading-one-pass2.js diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 742202b8c3..ddb5a86703 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -34,7 +34,7 @@ | 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-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 | +| 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 | | link-name | Ensures links have discernible text | cat.name-role-value, wcag2a, wcag111, wcag412, wcag244, section508, section508.22.a | true | @@ -46,6 +46,7 @@ | meta-viewport | Ensures <meta name="viewport"> does not disable text scaling and zooming | cat.sensory-and-visual-cues, wcag2aa, wcag144 | true | | object-alt | Ensures <object> elements have alternate text | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a | true | | p-as-heading | Ensure p elements are not used to style headings | cat.semantics, wcag2a, wcag131, experimental | true | +| page-has-heading-one | Ensure that the page, or at least one of its frames contains a level-one heading | best-practice | true | | radiogroup | Ensures related <input type="radio"> elements have a group and that the group designation is consistent | cat.forms, best-practice | true | | region | Ensures all content is contained within a landmark region | cat.keyboard, best-practice | true | | scope-attr-valid | Ensures the scope attribute is used correctly on tables | cat.tables, best-practice | true | diff --git a/lib/checks/keyboard/page-has-elm-after.js b/lib/checks/keyboard/page-has-elm-after.js index 2c78691956..87d0784247 100644 --- a/lib/checks/keyboard/page-has-elm-after.js +++ b/lib/checks/keyboard/page-has-elm-after.js @@ -2,8 +2,8 @@ const elmUsedAnywhere = results.some(frameResult => frameResult.result === true) // If the element exists in any frame, set them all to true if (elmUsedAnywhere) { - results.forEach(result => { - result.result = elmUsedAnywhere; - }); + results.forEach(result => { + result.result = true; + }); } -return results; \ No newline at end of file +return results; diff --git a/lib/checks/keyboard/page-has-elm.js b/lib/checks/keyboard/page-has-elm.js index 515f667549..2d524a3608 100644 --- a/lib/checks/keyboard/page-has-elm.js +++ b/lib/checks/keyboard/page-has-elm.js @@ -1,7 +1,7 @@ if (!options || !options.selector || typeof options.selector !== 'string') { - throw new TypeError('visible-in-page requires options.selector to be a string'); + throw new TypeError('visible-in-page requires options.selector to be a string'); } -const matchingElms = axe.utils.querySelectorAll(virtualNode, - options.selector); -return matchingElms && matchingElms.length > 0; \ No newline at end of file +const matchingElms = axe.utils.querySelectorAll(virtualNode, options.selector); +this.relatedNodes(matchingElms.map(vNode => vNode.actualNode)); +return matchingElms.length > 0; diff --git a/lib/checks/keyboard/page-has-heading-one.json b/lib/checks/keyboard/page-has-heading-one.json new file mode 100644 index 0000000000..06bdaa4c59 --- /dev/null +++ b/lib/checks/keyboard/page-has-heading-one.json @@ -0,0 +1,15 @@ +{ + "id": "page-has-heading-one", + "evaluate": "page-has-elm.js", + "after": "page-has-elm-after.js", + "options": { + "selector": "h1:not([role]), [role=\"heading\"][aria-level=\"1\"]" + }, + "metadata": { + "impact": "moderate", + "messages": { + "pass": "Page has at least one level-one heading", + "fail": "Page must have a level-one heading" + } + } +} \ No newline at end of file diff --git a/lib/rules/landmark-one-main.json b/lib/rules/landmark-one-main.json index c346c78258..fc04a99e9e 100644 --- a/lib/rules/landmark-one-main.json +++ b/lib/rules/landmark-one-main.json @@ -5,13 +5,13 @@ "best-practice" ], "metadata": { - "description": "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.", - "help": "Page must contain one main landmark." + "description": "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", + "help": "Page must contain one main landmark" }, "all": [ "page-has-main", "has-no-more-than-one-main" - ], + ], "any": [], "none": [] } \ No newline at end of file diff --git a/lib/rules/page-has-heading-one.json b/lib/rules/page-has-heading-one.json new file mode 100644 index 0000000000..bdc0a11742 --- /dev/null +++ b/lib/rules/page-has-heading-one.json @@ -0,0 +1,16 @@ +{ + "id": "page-has-heading-one", + "selector": "html", + "tags": [ + "best-practice" + ], + "metadata": { + "description": "Ensure that the page, or at least one of its frames contains a level-one heading", + "help": "Page must contain a level-one heading" + }, + "all": [ + "page-has-heading-one" + ], + "any": [], + "none": [] +} \ No newline at end of file diff --git a/test/checks/keyboard/page-has-elm.js b/test/checks/keyboard/page-has-elm.js index 68adedf81c..88afb45959 100644 --- a/test/checks/keyboard/page-has-elm.js +++ b/test/checks/keyboard/page-has-elm.js @@ -52,8 +52,9 @@ describe('page-has-*', function () { it('sets all results to true if any are true', function () { var results = [{ result: true }, { result: false }, { result: undefined }]; - assert.deepEqual(after(results), [{ result: true }, - { result: true }, { result: true }]); + assert.deepEqual(after(results), + [{ result: true }, { result: true }, { result: true }] + ); }); it('Leave the results as is if none of them were true', function () { @@ -96,4 +97,39 @@ describe('page-has-*', function () { assert.isTrue(mainIsFound); }); }); + + describe('page-has-heading-one', function () { + var check = checks['page-has-heading-one']; + + it('should return false if div has role not equal to heading', function() { + var params = checkSetup('
Wrong role
', check.options); + var h1IsFound = check.evaluate.apply(checkContext, params); + assert.isFalse(h1IsFound); + }); + + it('should return false if div has role heading but not aria-level=1', function() { + var params = checkSetup('
Wrong role
', check.options); + var h1IsFound = check.evaluate.apply(checkContext, params); + assert.isFalse(h1IsFound); + }); + + it('should return true if h1 exists', function(){ + var params = checkSetup('

My heading

', check.options); + var h1IsFound = check.evaluate.apply(checkContext, params); + assert.isTrue(h1IsFound); + }); + + it('should return true if a div has role=heading and aria-level=1', function() { + var params = checkSetup('
Diversity heading
', check.options); + var h1IsFound = check.evaluate.apply(checkContext, params); + assert.isTrue(h1IsFound); + }); + + (shadowSupported ? it : xit) + ('should return true if h1 is inside of shadow dom', function() { + var params = shadowCheckSetup('
', '

Shady Heading

', check.options); + var h1IsFound = check.evaluate.apply(checkContext, params); + assert.isTrue(h1IsFound); + }); + }); }); diff --git a/test/integration/full/page-has-heading-one/frames/level1-fail.html b/test/integration/full/page-has-heading-one/frames/level1-fail.html new file mode 100644 index 0000000000..d9f08c6845 --- /dev/null +++ b/test/integration/full/page-has-heading-one/frames/level1-fail.html @@ -0,0 +1,10 @@ + + + + + + + +

No h1 here either

+ + diff --git a/test/integration/full/page-has-heading-one/frames/level1.html b/test/integration/full/page-has-heading-one/frames/level1.html new file mode 100644 index 0000000000..07f7e210ee --- /dev/null +++ b/test/integration/full/page-has-heading-one/frames/level1.html @@ -0,0 +1,12 @@ + + + + + + + +

No h1 here either

+ + + + diff --git a/test/integration/full/page-has-heading-one/frames/level2-a.html b/test/integration/full/page-has-heading-one/frames/level2-a.html new file mode 100644 index 0000000000..ebd2950302 --- /dev/null +++ b/test/integration/full/page-has-heading-one/frames/level2-a.html @@ -0,0 +1,10 @@ + + + + + + + +

This page has an h1

+ + diff --git a/test/integration/full/page-has-heading-one/frames/level2.html b/test/integration/full/page-has-heading-one/frames/level2.html new file mode 100644 index 0000000000..deb2646894 --- /dev/null +++ b/test/integration/full/page-has-heading-one/frames/level2.html @@ -0,0 +1,10 @@ + + + + + + + +

No h1 content in this iframe

+ + diff --git a/test/integration/full/page-has-heading-one/page-has-heading-one-fail.html b/test/integration/full/page-has-heading-one/page-has-heading-one-fail.html new file mode 100644 index 0000000000..1fe5be70ae --- /dev/null +++ b/test/integration/full/page-has-heading-one/page-has-heading-one-fail.html @@ -0,0 +1,24 @@ + + + + + + + + + + + +

No h1 here

+ +
+ + + + diff --git a/test/integration/full/page-has-heading-one/page-has-heading-one-fail.js b/test/integration/full/page-has-heading-one/page-has-heading-one-fail.js new file mode 100644 index 0000000000..fbd10573d1 --- /dev/null +++ b/test/integration/full/page-has-heading-one/page-has-heading-one-fail.js @@ -0,0 +1,49 @@ +describe('page-has-heading-one test failure', function () { + 'use strict'; + var results; + before(function (done) { + function start() { + // Stop messing with my tests Mocha! + document.querySelector('#mocha h1').outerHTML = '

page-has-heading-one test

' + + axe.run({ runOnly: { type: 'rule', values: ['page-has-heading-one'] }}, function (err, r) { + assert.isNull(err); + results = r; + done(); + }); + } + if (document.readyState !== 'complete') { + window.addEventListener('load', start); + } else { + start(); + } + }); + + describe('violations', function () { + it('should find 1', function () { + assert.lengthOf(results.violations[0].nodes, 2); + }); + + it('should find #frame1', function () { + assert.deepEqual(results.violations[0].nodes[0].target, ['#fail1']); + }); + + it('should find #frame1, #violation2', function () { + assert.deepEqual(results.violations[0].nodes[1].target, ['#frame1', '#violation2']); + }); + }); + + describe('passes', function () { + it('should find 0', 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/page-has-heading-one/page-has-heading-one-pass1.html b/test/integration/full/page-has-heading-one/page-has-heading-one-pass1.html new file mode 100644 index 0000000000..353fac9ca0 --- /dev/null +++ b/test/integration/full/page-has-heading-one/page-has-heading-one-pass1.html @@ -0,0 +1,24 @@ + + + + + + + + + + + +

No h1 content

+ +
+ + + + diff --git a/test/integration/full/page-has-heading-one/page-has-heading-one-pass1.js b/test/integration/full/page-has-heading-one/page-has-heading-one-pass1.js new file mode 100644 index 0000000000..1d60c97452 --- /dev/null +++ b/test/integration/full/page-has-heading-one/page-has-heading-one-pass1.js @@ -0,0 +1,59 @@ +describe('page-has-heading-one test pass', function () { + 'use strict'; + var results; + before(function (done) { + function start() { + // Stop messing with my tests Mocha! + document.querySelector('#mocha h1').outerHTML = '

page-has-heading-one test

' + + axe.run({ runOnly: { type: 'rule', values: ['page-has-heading-one'] } }, function (err, r) { + assert.isNull(err); + results = r; + console.log(r); + done(); + }); + } + if (document.readyState !== 'complete') { + window.addEventListener('load', start); + } else { + start(); + } + }); + + 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 #pass1', function () { + assert.deepEqual(results.passes[0].nodes[0].target, ['#pass1']); + }); + + it('should find #frame1, #pass2', function () { + assert.deepEqual(results.passes[0].nodes[1].target, ['#frame1', '#pass2']); + }); + + it('should find #frame1, #frame2, #pass3', function () { + assert.deepEqual(results.passes[0].nodes[2].target, ['#frame1', '#frame2', '#pass3']); + }); + + it('should find #frame1, #frame3, #pass4', function () { + assert.deepEqual(results.passes[0].nodes[3].target, ['#frame1', '#frame3', '#pass4']); + }); + }); + + 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/page-has-heading-one/page-has-heading-one-pass2.html b/test/integration/full/page-has-heading-one/page-has-heading-one-pass2.html new file mode 100644 index 0000000000..5e747001b6 --- /dev/null +++ b/test/integration/full/page-has-heading-one/page-has-heading-one-pass2.html @@ -0,0 +1,23 @@ + + + + + + + + + + + +
Level one heading!
+
+ + + + diff --git a/test/integration/full/page-has-heading-one/page-has-heading-one-pass2.js b/test/integration/full/page-has-heading-one/page-has-heading-one-pass2.js new file mode 100644 index 0000000000..e1d92e3070 --- /dev/null +++ b/test/integration/full/page-has-heading-one/page-has-heading-one-pass2.js @@ -0,0 +1,46 @@ +describe('page-has-heading-one test pass', function () { + 'use strict'; + var results; + before(function (done) { + function start() { + // Stop messing with my tests Mocha! + document.querySelector('#mocha h1').outerHTML = '

page-has-heading-one test

' + + axe.run({ runOnly: { type: 'rule', values: ['page-has-heading-one'] } }, function (err, r) { + assert.isNull(err); + results = r; + done(); + }); + } + if (document.readyState !== 'complete') { + window.addEventListener('load', start); + } else { + start(); + } + }); + + describe('violations', function () { + it('should find 0', function () { + assert.lengthOf(results.violations, 0); + }); + }); + + describe('passes', function () { + it('should find 1', function () { + assert.lengthOf(results.passes[0].nodes, 1); + }); + + it('should find #pass1', function () { + assert.deepEqual(results.passes[0].nodes[0].target, ['#pass1']); + }); + }); + + it('should find 0 inapplicable', function () { + assert.lengthOf(results.inapplicable, 0); + }); + + it('should find 0 incomplete', function () { + assert.lengthOf(results.incomplete, 0); + }); + +});