From 261a72960934c69527eea5ae9da79e3d4d4a5bd8 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Tue, 31 Jul 2018 11:02:15 +0200 Subject: [PATCH 1/4] chore(WIP): Duplicate ID breakup --- doc/rule-descriptions.md | 16 ++-- .../shared/duplicate-id-accessible.json | 15 +++ lib/checks/shared/duplicate-id.js | 14 ++- lib/checks/shared/duplicate-id.json | 5 +- lib/commons/aria/is-accessible-ref.js | 53 ++++++++++ lib/rules/duplicate-id.json | 7 +- test/checks/shared/duplicate-id.js | 46 +++++++++ test/commons/aria/is-accessible-ref.js | 96 +++++++++++++++++++ 8 files changed, 237 insertions(+), 15 deletions(-) create mode 100644 lib/checks/shared/duplicate-id-accessible.json create mode 100644 lib/commons/aria/is-accessible-ref.js create mode 100644 test/commons/aria/is-accessible-ref.js diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 4ecd46a484..9516038d26 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -21,7 +21,7 @@ | definition-list | Ensures <dl> elements are structured correctly | Serious | cat.structure, wcag2a, wcag131 | true | | dlitem | Ensures <dt> and <dd> elements are contained by a <dl> | Serious | cat.structure, wcag2a, wcag131 | true | | document-title | Ensures each HTML document contains a non-empty <title> element | Serious | cat.text-alternatives, wcag2a, wcag242 | true | -| duplicate-id | Ensures every id attribute value is unique | Moderate | cat.parsing, wcag2a, wcag411 | true | +| duplicate-id | Ensures every id attribute value is unique | Minor, Serious | cat.parsing, wcag2a, wcag411 | true | | empty-heading | Ensures headings have discernible text | Minor | cat.name-role-value, best-practice | true | | focus-order-semantics | Ensures elements in the focus order have an appropriate role | Minor | cat.keyboard, best-practice, experimental | true | | frame-tested | Ensures <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, review-item | true | @@ -36,12 +36,12 @@ | input-image-alt | Ensures <input type="image"> elements have alternate text | Critical | 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 | Serious | cat.forms, best-practice | true | | label | Ensures every form element has a label | Minor, Serious, Critical | cat.forms, wcag2a, wcag332, wcag131, section508, section508.22.n | true | -| landmark-banner-is-top-level | The banner landmark should not be contained in another landmark | Moderate | cat.semantics, best-practice | true | -| landmark-contentinfo-is-top-level | The contentinfo landmark should not be contained in another landmark | Moderate | cat.semantics, best-practice | true | -| landmark-main-is-top-level | The main landmark should not be contained in another landmark | Moderate | cat.semantics, best-practice | true | -| landmark-no-duplicate-banner | Ensures the document has no more than one banner landmark | Moderate | cat.semantics, best-practice | true | -| landmark-no-duplicate-contentinfo | Ensures the document has no more than one contentinfo landmark | Moderate | cat.semantics, 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 | Moderate | cat.semantics, best-practice | true | +| landmark-banner-is-top-level | Ensures the banner landmark is at top level | Moderate | cat.semantics, best-practice | true | +| landmark-contentinfo-is-top-level | Ensures the contentinfo landmark is at top level | Moderate | cat.semantics, best-practice | true | +| landmark-main-is-top-level | Ensures the main landmark is at top level | Moderate | cat.semantics, best-practice | true | +| landmark-no-duplicate-banner | Ensures the page has at most one banner landmark | Moderate | cat.semantics, best-practice | true | +| landmark-no-duplicate-contentinfo | Ensures the page has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | true | +| landmark-one-main | Ensures the page has only one main landmark and each iframe in the page has at most one main landmark | Moderate | cat.semantics, best-practice | true | | layout-table | Ensures presentational <table> elements do not use <th>, <caption> elements or the summary attribute | Serious | cat.semantics, wcag2a, wcag131 | true | | link-in-text-block | Links can be distinguished without relying on color | Serious | cat.color, experimental, wcag2a, wcag141 | true | | link-name | Ensures links have discernible text | Serious | cat.name-role-value, wcag2a, wcag412, wcag244, section508, section508.22.a | true | @@ -55,7 +55,7 @@ | p-as-heading | Ensure p elements are not used to style headings | Serious | 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 | Moderate | cat.semantics, best-practice | true | | radiogroup | Ensures related <input type="radio"> elements have a group and that the group designation is consistent | Critical | cat.forms, best-practice | true | -| region | Ensures all content is contained within a landmark region | Moderate | cat.keyboard, best-practice | true | +| region | Ensures all page content is contained by landmarks | Moderate | cat.keyboard, best-practice | true | | scope-attr-valid | Ensures the scope attribute is used correctly on tables | Moderate, Critical | cat.tables, best-practice | true | | server-side-image-map | Ensures that server-side image maps are not used | Minor | cat.text-alternatives, wcag2a, wcag211, section508, section508.22.f | true | | skip-link | Ensure all skip links have a focusable target | Moderate | cat.keyboard, best-practice | true | diff --git a/lib/checks/shared/duplicate-id-accessible.json b/lib/checks/shared/duplicate-id-accessible.json new file mode 100644 index 0000000000..4b8163e42b --- /dev/null +++ b/lib/checks/shared/duplicate-id-accessible.json @@ -0,0 +1,15 @@ +{ + "id": "duplicate-id-accessible", + "evaluate": "duplicate-id.js", + "after": "duplicate-id-after.js", + "options": { + "accReferred": true + }, + "metadata": { + "impact": "serious", + "messages": { + "pass": "Document has no elements that share the same id attribute", + "fail": "Document has multiple elements with the same id attribute: {{=it.data}}" + } + } +} diff --git a/lib/checks/shared/duplicate-id.js b/lib/checks/shared/duplicate-id.js index fa0e3ed141..d504efe79d 100644 --- a/lib/checks/shared/duplicate-id.js +++ b/lib/checks/shared/duplicate-id.js @@ -1,12 +1,20 @@ +const { aria, dom, utils } = axe.commons; const id = node.getAttribute('id').trim(); +const { accReferred } = options || {}; + // Since empty ID's are not meaningful and are ignored by Edge, we'll // let those pass. -if (!id) { +if ( + !id || + (typeof accReferred === 'boolean' && + accReferred === !aria.isAccessibleRef(node)) +) { return true; } -const root = axe.commons.dom.getRootNode(node); + +const root = dom.getRootNode(node); const matchingNodes = Array.from( - root.querySelectorAll(`[id="${axe.commons.utils.escapeSelector(id)}"]`) + root.querySelectorAll(`[id="${utils.escapeSelector(id)}"]`) ).filter(foundNode => foundNode !== node); if (matchingNodes.length) { diff --git a/lib/checks/shared/duplicate-id.json b/lib/checks/shared/duplicate-id.json index 3d55e3fb1f..201efcc542 100644 --- a/lib/checks/shared/duplicate-id.json +++ b/lib/checks/shared/duplicate-id.json @@ -2,8 +2,11 @@ "id": "duplicate-id", "evaluate": "duplicate-id.js", "after": "duplicate-id-after.js", + "options": { + "accReferred": false + }, "metadata": { - "impact": "moderate", + "impact": "minor", "messages": { "pass": "Document has no elements that share the same id attribute", "fail": "Document has multiple elements with the same id attribute: {{=it.data}}" diff --git a/lib/commons/aria/is-accessible-ref.js b/lib/commons/aria/is-accessible-ref.js new file mode 100644 index 0000000000..e92aceac09 --- /dev/null +++ b/lib/commons/aria/is-accessible-ref.js @@ -0,0 +1,53 @@ +/* global aria, axe, dom */ +function findDomNode(node, functor) { + if (functor(node)) { + return node; + } + for (const child of node.children) { + const out = findDomNode(child, functor); + if (out) { + return out; + } + } +} + +/** + * Check that a DOM node is a reference in the accessibility tree + * @param {Element} node + * @returns {Boolean} + */ +aria.isAccessibleRef = function isAccessibleRef(node) { + node = node.actualNode || node; + let root = dom.getRootNode(node); + root = root.documentElement || root; // account for shadow roots + const id = node.id; + + // Get all idref(s) attributes on the lookup table + const refAttrs = Object.keys(aria.lookupTable.attributes).filter(attr => { + const { type } = aria.lookupTable.attributes[attr]; + return /^idrefs?$/.test(type); + }); + + // Find the first element that IDREF(S) the node + let refElm = findDomNode(root, elm => { + if (elm.nodeType !== 1) { + // Elements only + return; + } + if ( + elm.nodeName.toUpperCase() === 'LABEL' && + elm.getAttribute('for') === id + ) { + return true; + } + // See if there are any aria attributes that reference the node + return refAttrs.filter(attr => elm.hasAttribute(attr)).some(attr => { + const attrValue = elm.getAttribute(attr); + if (aria.lookupTable.attributes[attr].type === 'idref') { + return attrValue === id; + } + return axe.utils.tokenList(attrValue).includes(id); + }); + }); + return typeof refElm !== 'undefined'; +}; diff --git a/lib/rules/duplicate-id.json b/lib/rules/duplicate-id.json index 215238238f..4a103db048 100644 --- a/lib/rules/duplicate-id.json +++ b/lib/rules/duplicate-id.json @@ -11,9 +11,10 @@ "description": "Ensures every id attribute value is unique", "help": "id attribute value must be unique" }, - "all": [], - "any": [ - "duplicate-id" + "all": [ + "duplicate-id", + "duplicate-id-accessible" ], + "any": [], "none": [] } diff --git a/test/checks/shared/duplicate-id.js b/test/checks/shared/duplicate-id.js index 71b5216db8..9e745ef85e 100644 --- a/test/checks/shared/duplicate-id.js +++ b/test/checks/shared/duplicate-id.js @@ -116,4 +116,50 @@ describe('duplicate-id', function() { assert.deepEqual(checkContext._relatedNodes, [node.querySelector('p')]); } ); + + describe('options.accReferred', function() { + it('ignores unreffed elements with accReferred: true', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('div[id="foo"]'); + assert.isTrue( + checks['duplicate-id'].evaluate.call(checkContext, node, { + accReferred: true + }) + ); + }); + + it('tests reffed elements with accReferred: true', function() { + fixture.innerHTML = + '
' + + '
'; + var node = fixture.querySelector('div[id="foo"]'); + assert.isFalse( + checks['duplicate-id'].evaluate.call(checkContext, node, { + accReferred: true + }) + ); + }); + + it('ignores reffed elements with accReferred: false', function() { + fixture.innerHTML = + '
' + + '
'; + var node = fixture.querySelector('div[id="foo"]'); + assert.isTrue( + checks['duplicate-id'].evaluate.call(checkContext, node, { + accReferred: false + }) + ); + }); + + it('tests unreffed elements with accReferred: false', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('div[id="foo"]'); + assert.isFalse( + checks['duplicate-id'].evaluate.call(checkContext, node, { + accReferred: false + }) + ); + }); + }); }); diff --git a/test/commons/aria/is-accessible-ref.js b/test/commons/aria/is-accessible-ref.js new file mode 100644 index 0000000000..177c5352fa --- /dev/null +++ b/test/commons/aria/is-accessible-ref.js @@ -0,0 +1,96 @@ +describe('aria.isAccessibleRef', function() { + 'use strict'; + + var __atrs; + var fixture = document.getElementById('fixture'); + var isAccessibleRef = axe.commons.aria.isAccessibleRef; + var shadowSupport = axe.testUtils.shadowSupport; + + function setLookup(attrs) { + axe.commons.aria.lookupTable.attributes = attrs; + } + + afterEach(function() { + fixture.innerHTML = ''; + __atrs = axe.commons.aria.lookupTable.attributes; + }); + + afterEach(function() { + axe.commons.aria.lookupTable.attributes = __atrs; + }); + + it('returns false by default', function() { + fixture.innerHTML = '
'; + var node = document.getElementById('foo'); + assert.isFalse(isAccessibleRef(node)); + }); + + it('returns true for IDs used in aria IDREF attributes', function() { + setLookup({ 'aria-foo': { type: 'idref' } }); + fixture.innerHTML = '
'; + var node = document.getElementById('foo'); + assert.isTrue(isAccessibleRef(node)); + }); + + it('returns true for IDs used in aria IDREFS attributes', function() { + setLookup({ 'aria-bar': { type: 'idrefs' } }); + fixture.innerHTML = + '
'; + + var node1 = document.getElementById('foo'); + var node2 = document.getElementById('bar'); + assert.isTrue(isAccessibleRef(node1)); + assert.isTrue(isAccessibleRef(node2)); + }); + + it('returns true for IDs used in label[for] attributes', function() { + setLookup({ 'aria-foo': { type: 'idref' } }); + fixture.innerHTML = ''; + var node = document.getElementById('baz'); + assert.isTrue(isAccessibleRef(node)); + }); + + (shadowSupport ? it : xit)('works inside shadow DOM', function() { + setLookup({ 'aria-bar': { type: 'idref' } }); + fixture.innerHTML = '
'; + + var shadow = document.getElementById('foo').attachShadow({ mode: 'open' }); + shadow.innerHTML = '
'; + + var node = shadow.getElementById('bar'); + assert.isTrue(isAccessibleRef(node)); + }); + + (shadowSupport ? it : xit)( + 'returns false for IDREFs inside shadow DOM', + function() { + setLookup({ 'aria-foo': { type: 'idrefs' } }); + fixture.innerHTML = '
'; + var node1 = document.getElementById('foo'); + var node2 = document.getElementById('bar'); + + var shadow = node1.attachShadow({ mode: 'open' }); + shadow.innerHTML = '
'; + + assert.isFalse(isAccessibleRef(node1)); + assert.isFalse(isAccessibleRef(node2)); + } + ); + + (shadowSupport ? it : xit)( + 'returns false for IDREFs outside shadow DOM', + function() { + setLookup({ 'aria-bar': { type: 'idref' } }); + fixture.innerHTML = + '
'; + + var shadow = document + .getElementById('foo') + .attachShadow({ mode: 'open' }); + shadow.innerHTML = '
'; + + var node = shadow.getElementById('bar'); + assert.isFalse(isAccessibleRef(node)); + } + ); +}); From 2ecfea71a164bd039d52ea76ecd0c6a8814e2405 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Fri, 24 Aug 2018 14:25:16 +0200 Subject: [PATCH 2/4] feat: Break up duplicate-id rule for ARIA+labels and active elements --- doc/rule-descriptions.md | 4 +- .../duplicate-id-active.json} | 5 +- .../{shared => parsing}/duplicate-id-after.js | 0 lib/checks/parsing/duplicate-id-aria.json | 12 +++ .../{shared => parsing}/duplicate-id.js | 11 +-- .../{shared => parsing}/duplicate-id.json | 3 - lib/commons/aria/is-accessible-ref.js | 4 +- lib/rules/duplicate-id-active-matches.js | 8 ++ lib/rules/duplicate-id-active.json | 20 +++++ lib/rules/duplicate-id-aria-matches.js | 1 + lib/rules/duplicate-id-aria.json | 20 +++++ lib/rules/duplicate-id-misc-matches.js | 11 +++ lib/rules/duplicate-id.json | 4 +- .../checks/{shared => parser}/duplicate-id.js | 46 ----------- test/commons/aria/is-accessible-ref.js | 2 +- .../duplicate-id-active.html | 19 +++++ .../duplicate-id-active.json | 6 ++ .../duplicate-id-aria/duplicate-id-aria.html | 19 +++++ .../duplicate-id-aria/duplicate-id-aria.json | 6 ++ .../rules/duplicate-id/duplicate-id.html | 18 ++++- .../rules/duplicate-id/duplicate-id.json | 4 +- .../duplicate-id-active-matches.js | 79 +++++++++++++++++++ .../duplicate-id-aria-matches.2.js | 79 +++++++++++++++++++ .../rule-matches/duplicate-id-misc-matches.js | 79 +++++++++++++++++++ 24 files changed, 388 insertions(+), 72 deletions(-) rename lib/checks/{shared/duplicate-id-accessible.json => parsing/duplicate-id-active.json} (79%) rename lib/checks/{shared => parsing}/duplicate-id-after.js (100%) create mode 100644 lib/checks/parsing/duplicate-id-aria.json rename lib/checks/{shared => parsing}/duplicate-id.js (70%) rename lib/checks/{shared => parsing}/duplicate-id.json (87%) create mode 100644 lib/rules/duplicate-id-active-matches.js create mode 100644 lib/rules/duplicate-id-active.json create mode 100644 lib/rules/duplicate-id-aria-matches.js create mode 100644 lib/rules/duplicate-id-aria.json create mode 100644 lib/rules/duplicate-id-misc-matches.js rename test/checks/{shared => parser}/duplicate-id.js (73%) create mode 100644 test/integration/rules/duplicate-id-active/duplicate-id-active.html create mode 100644 test/integration/rules/duplicate-id-active/duplicate-id-active.json create mode 100644 test/integration/rules/duplicate-id-aria/duplicate-id-aria.html create mode 100644 test/integration/rules/duplicate-id-aria/duplicate-id-aria.json create mode 100644 test/rule-matches/duplicate-id-active-matches.js create mode 100644 test/rule-matches/duplicate-id-aria-matches.2.js create mode 100644 test/rule-matches/duplicate-id-misc-matches.js diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 8ce525d6bc..c3afebc390 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -23,7 +23,9 @@ | definition-list | Ensures <dl> elements are structured correctly | Serious | cat.structure, wcag2a, wcag131 | true | | dlitem | Ensures <dt> and <dd> elements are contained by a <dl> | Serious | cat.structure, wcag2a, wcag131 | true | | document-title | Ensures each HTML document contains a non-empty <title> element | Serious | cat.text-alternatives, wcag2a, wcag242 | true | -| duplicate-id | Ensures every id attribute value is unique | Minor, Serious | cat.parsing, wcag2a, wcag411 | true | +| duplicate-id-active | Ensures every id attribute value is unique | Serious | cat.parsing, wcag2a, wcag411 | true | +| duplicate-id-aria | Ensures every id attribute value used in ARIA and in labels is unique | Critical | cat.parsing, wcag2a, wcag411 | true | +| duplicate-id | Ensures every id attribute value is unique | Minor | cat.parsing, wcag2a, wcag411 | true | | empty-heading | Ensures headings have discernible text | Minor | cat.name-role-value, best-practice | true | | focus-order-semantics | Ensures elements in the focus order have an appropriate role | Minor | cat.keyboard, best-practice, experimental | true | | frame-tested | Ensures <iframe> and <frame> elements contain the axe-core script | Critical | cat.structure, review-item | true | diff --git a/lib/checks/shared/duplicate-id-accessible.json b/lib/checks/parsing/duplicate-id-active.json similarity index 79% rename from lib/checks/shared/duplicate-id-accessible.json rename to lib/checks/parsing/duplicate-id-active.json index 4b8163e42b..5ef2f179d3 100644 --- a/lib/checks/shared/duplicate-id-accessible.json +++ b/lib/checks/parsing/duplicate-id-active.json @@ -1,10 +1,7 @@ { - "id": "duplicate-id-accessible", + "id": "duplicate-id-active", "evaluate": "duplicate-id.js", "after": "duplicate-id-after.js", - "options": { - "accReferred": true - }, "metadata": { "impact": "serious", "messages": { diff --git a/lib/checks/shared/duplicate-id-after.js b/lib/checks/parsing/duplicate-id-after.js similarity index 100% rename from lib/checks/shared/duplicate-id-after.js rename to lib/checks/parsing/duplicate-id-after.js diff --git a/lib/checks/parsing/duplicate-id-aria.json b/lib/checks/parsing/duplicate-id-aria.json new file mode 100644 index 0000000000..26de80fedb --- /dev/null +++ b/lib/checks/parsing/duplicate-id-aria.json @@ -0,0 +1,12 @@ +{ + "id": "duplicate-id-aria", + "evaluate": "duplicate-id.js", + "after": "duplicate-id-after.js", + "metadata": { + "impact": "critical", + "messages": { + "pass": "Document has no elements that share the same id attribute", + "fail": "Document has multiple elements with the same id attribute: {{=it.data}}" + } + } +} diff --git a/lib/checks/shared/duplicate-id.js b/lib/checks/parsing/duplicate-id.js similarity index 70% rename from lib/checks/shared/duplicate-id.js rename to lib/checks/parsing/duplicate-id.js index d504efe79d..422fd52a29 100644 --- a/lib/checks/shared/duplicate-id.js +++ b/lib/checks/parsing/duplicate-id.js @@ -1,18 +1,13 @@ -const { aria, dom, utils } = axe.commons; +const { dom, utils } = axe.commons; const id = node.getAttribute('id').trim(); -const { accReferred } = options || {}; +const root = dom.getRootNode(node); // Since empty ID's are not meaningful and are ignored by Edge, we'll // let those pass. -if ( - !id || - (typeof accReferred === 'boolean' && - accReferred === !aria.isAccessibleRef(node)) -) { +if (!id) { return true; } -const root = dom.getRootNode(node); const matchingNodes = Array.from( root.querySelectorAll(`[id="${utils.escapeSelector(id)}"]`) ).filter(foundNode => foundNode !== node); diff --git a/lib/checks/shared/duplicate-id.json b/lib/checks/parsing/duplicate-id.json similarity index 87% rename from lib/checks/shared/duplicate-id.json rename to lib/checks/parsing/duplicate-id.json index 201efcc542..e611b041a6 100644 --- a/lib/checks/shared/duplicate-id.json +++ b/lib/checks/parsing/duplicate-id.json @@ -2,9 +2,6 @@ "id": "duplicate-id", "evaluate": "duplicate-id.js", "after": "duplicate-id-after.js", - "options": { - "accReferred": false - }, "metadata": { "impact": "minor", "messages": { diff --git a/lib/commons/aria/is-accessible-ref.js b/lib/commons/aria/is-accessible-ref.js index e92aceac09..f743fd798a 100644 --- a/lib/commons/aria/is-accessible-ref.js +++ b/lib/commons/aria/is-accessible-ref.js @@ -3,8 +3,8 @@ function findDomNode(node, functor) { if (functor(node)) { return node; } - for (const child of node.children) { - const out = findDomNode(child, functor); + for (let i = 0; i < node.children.length; i++) { + const out = findDomNode(node.children[i], functor); if (out) { return out; } diff --git a/lib/rules/duplicate-id-active-matches.js b/lib/rules/duplicate-id-active-matches.js new file mode 100644 index 0000000000..1a0e025283 --- /dev/null +++ b/lib/rules/duplicate-id-active-matches.js @@ -0,0 +1,8 @@ +const { dom, aria } = axe.commons; +const id = node.getAttribute('id').trim(); +const idSelector = `*[id="${axe.utils.escapeSelector(id)}"]`; +const idMatchingElms = Array.from( + dom.getRootNode(node).querySelectorAll(idSelector) +); + +return idMatchingElms.some(dom.isFocusable) && !aria.isAccessibleRef(node); diff --git a/lib/rules/duplicate-id-active.json b/lib/rules/duplicate-id-active.json new file mode 100644 index 0000000000..94daba5328 --- /dev/null +++ b/lib/rules/duplicate-id-active.json @@ -0,0 +1,20 @@ +{ + "id": "duplicate-id-active", + "selector": "[id]", + "matches": "duplicate-id-active-matches.js", + "excludeHidden": false, + "tags": [ + "cat.parsing", + "wcag2a", + "wcag411" + ], + "metadata": { + "description": "Ensures every id attribute value is unique", + "help": "IDs of active elements must be unique" + }, + "all": [ + "duplicate-id-active" + ], + "any": [], + "none": [] +} diff --git a/lib/rules/duplicate-id-aria-matches.js b/lib/rules/duplicate-id-aria-matches.js new file mode 100644 index 0000000000..a91461fc34 --- /dev/null +++ b/lib/rules/duplicate-id-aria-matches.js @@ -0,0 +1 @@ +return axe.commons.aria.isAccessibleRef(node); diff --git a/lib/rules/duplicate-id-aria.json b/lib/rules/duplicate-id-aria.json new file mode 100644 index 0000000000..e9e655e0f1 --- /dev/null +++ b/lib/rules/duplicate-id-aria.json @@ -0,0 +1,20 @@ +{ + "id": "duplicate-id-aria", + "selector": "[id]", + "matches": "duplicate-id-aria-matches.js", + "excludeHidden": false, + "tags": [ + "cat.parsing", + "wcag2a", + "wcag411" + ], + "metadata": { + "description": "Ensures every id attribute value used in ARIA and in labels is unique", + "help": "IDs used in ARIA and labels must be unique" + }, + "all": [ + "duplicate-id-aria" + ], + "any": [], + "none": [] +} diff --git a/lib/rules/duplicate-id-misc-matches.js b/lib/rules/duplicate-id-misc-matches.js new file mode 100644 index 0000000000..3825053977 --- /dev/null +++ b/lib/rules/duplicate-id-misc-matches.js @@ -0,0 +1,11 @@ +const { dom, aria } = axe.commons; +const id = node.getAttribute('id').trim(); +const idSelector = `*[id="${axe.utils.escapeSelector(id)}"]`; +const idMatchingElms = Array.from( + dom.getRootNode(node).querySelectorAll(idSelector) +); + +return ( + idMatchingElms.every(elm => !dom.isFocusable(elm)) && + !aria.isAccessibleRef(node) +); diff --git a/lib/rules/duplicate-id.json b/lib/rules/duplicate-id.json index 4a103db048..8e2b4b5707 100644 --- a/lib/rules/duplicate-id.json +++ b/lib/rules/duplicate-id.json @@ -1,6 +1,7 @@ { "id": "duplicate-id", "selector": "[id]", + "matches": "duplicate-id-misc-matches.js", "excludeHidden": false, "tags": [ "cat.parsing", @@ -12,8 +13,7 @@ "help": "id attribute value must be unique" }, "all": [ - "duplicate-id", - "duplicate-id-accessible" + "duplicate-id" ], "any": [], "none": [] diff --git a/test/checks/shared/duplicate-id.js b/test/checks/parser/duplicate-id.js similarity index 73% rename from test/checks/shared/duplicate-id.js rename to test/checks/parser/duplicate-id.js index 9e745ef85e..71b5216db8 100644 --- a/test/checks/shared/duplicate-id.js +++ b/test/checks/parser/duplicate-id.js @@ -116,50 +116,4 @@ describe('duplicate-id', function() { assert.deepEqual(checkContext._relatedNodes, [node.querySelector('p')]); } ); - - describe('options.accReferred', function() { - it('ignores unreffed elements with accReferred: true', function() { - fixture.innerHTML = '
'; - var node = fixture.querySelector('div[id="foo"]'); - assert.isTrue( - checks['duplicate-id'].evaluate.call(checkContext, node, { - accReferred: true - }) - ); - }); - - it('tests reffed elements with accReferred: true', function() { - fixture.innerHTML = - '
' + - '
'; - var node = fixture.querySelector('div[id="foo"]'); - assert.isFalse( - checks['duplicate-id'].evaluate.call(checkContext, node, { - accReferred: true - }) - ); - }); - - it('ignores reffed elements with accReferred: false', function() { - fixture.innerHTML = - '
' + - '
'; - var node = fixture.querySelector('div[id="foo"]'); - assert.isTrue( - checks['duplicate-id'].evaluate.call(checkContext, node, { - accReferred: false - }) - ); - }); - - it('tests unreffed elements with accReferred: false', function() { - fixture.innerHTML = '
'; - var node = fixture.querySelector('div[id="foo"]'); - assert.isFalse( - checks['duplicate-id'].evaluate.call(checkContext, node, { - accReferred: false - }) - ); - }); - }); }); diff --git a/test/commons/aria/is-accessible-ref.js b/test/commons/aria/is-accessible-ref.js index 177c5352fa..4d926cf710 100644 --- a/test/commons/aria/is-accessible-ref.js +++ b/test/commons/aria/is-accessible-ref.js @@ -4,7 +4,7 @@ describe('aria.isAccessibleRef', function() { var __atrs; var fixture = document.getElementById('fixture'); var isAccessibleRef = axe.commons.aria.isAccessibleRef; - var shadowSupport = axe.testUtils.shadowSupport; + var shadowSupport = axe.testUtils.shadowSupport.v1; function setLookup(attrs) { axe.commons.aria.lookupTable.attributes = attrs; diff --git a/test/integration/rules/duplicate-id-active/duplicate-id-active.html b/test/integration/rules/duplicate-id-active/duplicate-id-active.html new file mode 100644 index 0000000000..f82aa81633 --- /dev/null +++ b/test/integration/rules/duplicate-id-active/duplicate-id-active.html @@ -0,0 +1,19 @@ +

This is my first paragraph with this ID.

+
+

This is my second paragraph with this ID.

+
+ + + + + + + +
+ + + +
+ + + \ No newline at end of file diff --git a/test/integration/rules/duplicate-id-active/duplicate-id-active.json b/test/integration/rules/duplicate-id-active/duplicate-id-active.json new file mode 100644 index 0000000000..d9ef605f83 --- /dev/null +++ b/test/integration/rules/duplicate-id-active/duplicate-id-active.json @@ -0,0 +1,6 @@ +{ + "description": "duplicate-id-active test", + "rule": "duplicate-id-active", + "violations": [[".fail1"]], + "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"]] +} diff --git a/test/integration/rules/duplicate-id-aria/duplicate-id-aria.html b/test/integration/rules/duplicate-id-aria/duplicate-id-aria.html new file mode 100644 index 0000000000..7aece2bec6 --- /dev/null +++ b/test/integration/rules/duplicate-id-aria/duplicate-id-aria.html @@ -0,0 +1,19 @@ +

This is my first paragraph with this ID.

+
+

This is my second paragraph with this ID.

+
+ + + + +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/test/integration/rules/duplicate-id-aria/duplicate-id-aria.json b/test/integration/rules/duplicate-id-aria/duplicate-id-aria.json new file mode 100644 index 0000000000..9d2a7d4737 --- /dev/null +++ b/test/integration/rules/duplicate-id-aria/duplicate-id-aria.json @@ -0,0 +1,6 @@ +{ + "description": "duplicate-id-aria test", + "rule": "duplicate-id-aria", + "violations": [[".fail1"]], + "passes": [["#pass1"], ["#pass2"]] +} diff --git a/test/integration/rules/duplicate-id/duplicate-id.html b/test/integration/rules/duplicate-id/duplicate-id.html index 5260c063b8..0c9fb0fd16 100644 --- a/test/integration/rules/duplicate-id/duplicate-id.html +++ b/test/integration/rules/duplicate-id/duplicate-id.html @@ -1,5 +1,17 @@ -

This is my first paragraph with this ID.

+

This is my first paragraph with this ID.

-

This is my second paragraph with this ID.

+

This is my second paragraph with this ID.

-

This is my only paragraph with this ID.

+ + + + +
+ + + +
+ + + + \ No newline at end of file diff --git a/test/integration/rules/duplicate-id/duplicate-id.json b/test/integration/rules/duplicate-id/duplicate-id.json index baa508d0e1..983ab00e75 100644 --- a/test/integration/rules/duplicate-id/duplicate-id.json +++ b/test/integration/rules/duplicate-id/duplicate-id.json @@ -1,6 +1,6 @@ { "description": "duplicate-id test", "rule": "duplicate-id", - "violations": [["#fixture > p:nth-child(1)"]], - "passes": [["#fixture"], ["#single"]] + "violations": [[".fail1"]], + "passes": [["#fixture"], ["#pass1"], ["#pass2"], ["#pass3"]] } diff --git a/test/rule-matches/duplicate-id-active-matches.js b/test/rule-matches/duplicate-id-active-matches.js new file mode 100644 index 0000000000..8918934eee --- /dev/null +++ b/test/rule-matches/duplicate-id-active-matches.js @@ -0,0 +1,79 @@ +describe('duplicate-id-mismatches', function() { + 'use strict'; + + var fixture = document.getElementById('fixture'); + var fixtureSetup = axe.testUtils.fixtureSetup; + var rule; + + beforeEach(function() { + rule = axe._audit.rules.find(function(rule) { + return rule.id === 'duplicate-id-active'; + }); + }); + + afterEach(function() { + fixture.innerHTML = ''; + }); + + it('is a function', function() { + assert.isFunction(rule.matches); + }); + + it('returns false if the ID is of an inactive non-referenced element', function() { + fixtureSetup('
'); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'div[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns false if the ID is of an inactive non-referenced element with a duplicate', function() { + fixtureSetup('
'); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'span[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns true if the ID is of an active non-referenced element', function() { + fixtureSetup(''); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'button[id=foo]')[0]; + assert.isTrue(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns true if the ID is a duplicate of an active non-referenced element', function() { + fixtureSetup('
' + ''); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'div[id=foo]')[0]; + assert.isTrue(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns false if the ID is of an inactive ARIA referenced element', function() { + fixtureSetup('
' + '
'); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'div[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns false if the ID is a duplicate of an inactive ARIA referenced element', function() { + fixtureSetup( + '
' + + '
' + + '' + ); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'span[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns false if the ID is of an active ARIA referenced element', function() { + fixtureSetup( + '' + '
' + ); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'button[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns false if the ID is a duplicate of of an active ARIA referenced element', function() { + fixtureSetup( + '' + + '
' + + '' + ); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'span[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); +}); diff --git a/test/rule-matches/duplicate-id-aria-matches.2.js b/test/rule-matches/duplicate-id-aria-matches.2.js new file mode 100644 index 0000000000..c6c49719de --- /dev/null +++ b/test/rule-matches/duplicate-id-aria-matches.2.js @@ -0,0 +1,79 @@ +describe('duplicate-id-mismatches', function() { + 'use strict'; + + var fixture = document.getElementById('fixture'); + var fixtureSetup = axe.testUtils.fixtureSetup; + var rule; + + beforeEach(function() { + rule = axe._audit.rules.find(function(rule) { + return rule.id === 'duplicate-id-aria'; + }); + }); + + afterEach(function() { + fixture.innerHTML = ''; + }); + + it('is a function', function() { + assert.isFunction(rule.matches); + }); + + it('returns false if the ID is of an inactive non-referenced element', function() { + fixtureSetup('
'); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'div[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns false if the ID is of an inactive non-referenced element with a duplicate', function() { + fixtureSetup('
'); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'span[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns false if the ID is of an active non-referenced element', function() { + fixtureSetup(''); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'button[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns false if the ID is a duplicate of an active non-referenced element', function() { + fixtureSetup('
' + ''); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'div[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns true if the ID is of an inactive ARIA referenced element', function() { + fixtureSetup('
' + '
'); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'div[id=foo]')[0]; + assert.isTrue(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns true if the ID is a duplicate of an inactive ARIA referenced element', function() { + fixtureSetup( + '
' + + '
' + + '' + ); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'span[id=foo]')[0]; + assert.isTrue(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns true if the ID is of an active ARIA referenced element', function() { + fixtureSetup( + '' + '
' + ); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'button[id=foo]')[0]; + assert.isTrue(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns true if the ID is a duplicate of of an active ARIA referenced element', function() { + fixtureSetup( + '' + + '
' + + '' + ); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'span[id=foo]')[0]; + assert.isTrue(rule.matches(vNode.actualNode, vNode)); + }); +}); diff --git a/test/rule-matches/duplicate-id-misc-matches.js b/test/rule-matches/duplicate-id-misc-matches.js new file mode 100644 index 0000000000..c3b8ffffda --- /dev/null +++ b/test/rule-matches/duplicate-id-misc-matches.js @@ -0,0 +1,79 @@ +describe('duplicate-id-mismatches', function() { + 'use strict'; + + var fixture = document.getElementById('fixture'); + var fixtureSetup = axe.testUtils.fixtureSetup; + var rule; + + beforeEach(function() { + rule = axe._audit.rules.find(function(rule) { + return rule.id === 'duplicate-id'; + }); + }); + + afterEach(function() { + fixture.innerHTML = ''; + }); + + it('is a function', function() { + assert.isFunction(rule.matches); + }); + + it('returns true if the ID is of an inactive non-referenced element', function() { + fixtureSetup('
'); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'div[id=foo]')[0]; + assert.isTrue(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns true if the ID is of an inactive non-referenced element with a duplicate', function() { + fixtureSetup('
'); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'span[id=foo]')[0]; + assert.isTrue(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns false if the ID is of an active non-referenced element', function() { + fixtureSetup(''); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'button[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns false if the ID is a duplicate of an active non-referenced element', function() { + fixtureSetup('
' + ''); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'div[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns false if the ID is of an inactive ARIA referenced element', function() { + fixtureSetup('
' + '
'); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'div[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns false if the ID is a duplicate of an inactive ARIA referenced element', function() { + fixtureSetup( + '
' + + '
' + + '' + ); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'span[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns false if the ID is of an active ARIA referenced element', function() { + fixtureSetup( + '' + '
' + ); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'button[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); + + it('returns false if the ID is a duplicate of of an active ARIA referenced element', function() { + fixtureSetup( + '' + + '
' + + '' + ); + var vNode = axe.utils.querySelectorAll(axe._tree[0], 'span[id=foo]')[0]; + assert.isFalse(rule.matches(vNode.actualNode, vNode)); + }); +}); From 22d5ba79ceeca800f1af6d0b6f8f300e84f30ab3 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Fri, 24 Aug 2018 14:34:37 +0200 Subject: [PATCH 3/4] chore: duplicate-id editorial --- doc/rule-descriptions.md | 2 +- lib/checks/parsing/duplicate-id-active.json | 4 ++-- lib/checks/parsing/duplicate-id-aria.json | 4 ++-- lib/checks/parsing/duplicate-id.js | 7 ++----- lib/rules/duplicate-id-active.json | 6 +++--- lib/rules/duplicate-id-aria.json | 4 ++-- lib/rules/duplicate-id.json | 4 ++-- 7 files changed, 14 insertions(+), 17 deletions(-) diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index c3afebc390..88dfac245c 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -23,7 +23,7 @@ | definition-list | Ensures <dl> elements are structured correctly | Serious | cat.structure, wcag2a, wcag131 | true | | dlitem | Ensures <dt> and <dd> elements are contained by a <dl> | Serious | cat.structure, wcag2a, wcag131 | true | | document-title | Ensures each HTML document contains a non-empty <title> element | Serious | cat.text-alternatives, wcag2a, wcag242 | true | -| duplicate-id-active | Ensures every id attribute value is unique | Serious | cat.parsing, wcag2a, wcag411 | true | +| duplicate-id-active | Ensures every id attribute value of active elements is unique | Serious | cat.parsing, wcag2a, wcag411 | true | | duplicate-id-aria | Ensures every id attribute value used in ARIA and in labels is unique | Critical | cat.parsing, wcag2a, wcag411 | true | | duplicate-id | Ensures every id attribute value is unique | Minor | cat.parsing, wcag2a, wcag411 | true | | empty-heading | Ensures headings have discernible text | Minor | cat.name-role-value, best-practice | true | diff --git a/lib/checks/parsing/duplicate-id-active.json b/lib/checks/parsing/duplicate-id-active.json index 5ef2f179d3..91c82c6fda 100644 --- a/lib/checks/parsing/duplicate-id-active.json +++ b/lib/checks/parsing/duplicate-id-active.json @@ -5,8 +5,8 @@ "metadata": { "impact": "serious", "messages": { - "pass": "Document has no elements that share the same id attribute", - "fail": "Document has multiple elements with the same id attribute: {{=it.data}}" + "pass": "Document has no active elements that share the same id attribute", + "fail": "Document has active elements with the same id attribute: {{=it.data}}" } } } diff --git a/lib/checks/parsing/duplicate-id-aria.json b/lib/checks/parsing/duplicate-id-aria.json index 26de80fedb..3d60e13d06 100644 --- a/lib/checks/parsing/duplicate-id-aria.json +++ b/lib/checks/parsing/duplicate-id-aria.json @@ -5,8 +5,8 @@ "metadata": { "impact": "critical", "messages": { - "pass": "Document has no elements that share the same id attribute", - "fail": "Document has multiple elements with the same id attribute: {{=it.data}}" + "pass": "Document has no elements referenced with ARIA or labels that share the same id attribute", + "fail": "Document has multiple elements referenced with ARIA or with the same id attribute: {{=it.data}}" } } } diff --git a/lib/checks/parsing/duplicate-id.js b/lib/checks/parsing/duplicate-id.js index 422fd52a29..fa0e3ed141 100644 --- a/lib/checks/parsing/duplicate-id.js +++ b/lib/checks/parsing/duplicate-id.js @@ -1,15 +1,12 @@ -const { dom, utils } = axe.commons; const id = node.getAttribute('id').trim(); -const root = dom.getRootNode(node); - // Since empty ID's are not meaningful and are ignored by Edge, we'll // let those pass. if (!id) { return true; } - +const root = axe.commons.dom.getRootNode(node); const matchingNodes = Array.from( - root.querySelectorAll(`[id="${utils.escapeSelector(id)}"]`) + root.querySelectorAll(`[id="${axe.commons.utils.escapeSelector(id)}"]`) ).filter(foundNode => foundNode !== node); if (matchingNodes.length) { diff --git a/lib/rules/duplicate-id-active.json b/lib/rules/duplicate-id-active.json index 94daba5328..219ce13dc4 100644 --- a/lib/rules/duplicate-id-active.json +++ b/lib/rules/duplicate-id-active.json @@ -9,12 +9,12 @@ "wcag411" ], "metadata": { - "description": "Ensures every id attribute value is unique", + "description": "Ensures every id attribute value of active elements is unique", "help": "IDs of active elements must be unique" }, - "all": [ + "all": [], + "any": [ "duplicate-id-active" ], - "any": [], "none": [] } diff --git a/lib/rules/duplicate-id-aria.json b/lib/rules/duplicate-id-aria.json index e9e655e0f1..6300d94aa7 100644 --- a/lib/rules/duplicate-id-aria.json +++ b/lib/rules/duplicate-id-aria.json @@ -12,9 +12,9 @@ "description": "Ensures every id attribute value used in ARIA and in labels is unique", "help": "IDs used in ARIA and labels must be unique" }, - "all": [ + "all": [], + "any": [ "duplicate-id-aria" ], - "any": [], "none": [] } diff --git a/lib/rules/duplicate-id.json b/lib/rules/duplicate-id.json index 8e2b4b5707..fd788dd41e 100644 --- a/lib/rules/duplicate-id.json +++ b/lib/rules/duplicate-id.json @@ -12,9 +12,9 @@ "description": "Ensures every id attribute value is unique", "help": "id attribute value must be unique" }, - "all": [ + "all": [], + "any": [ "duplicate-id" ], - "any": [], "none": [] } From 5a855df3aed47605797e0308160e3c55060127a7 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Fri, 24 Aug 2018 15:30:28 +0200 Subject: [PATCH 4/4] chore: Update duplicate-id based on feedback --- lib/checks/parsing/duplicate-id-aria.json | 2 +- lib/checks/parsing/duplicate-id.json | 4 ++-- .../rules/duplicate-id-active/duplicate-id-active.html | 8 ++++++++ .../rules/duplicate-id-active/duplicate-id-active.json | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/checks/parsing/duplicate-id-aria.json b/lib/checks/parsing/duplicate-id-aria.json index 3d60e13d06..ce0915e0d7 100644 --- a/lib/checks/parsing/duplicate-id-aria.json +++ b/lib/checks/parsing/duplicate-id-aria.json @@ -6,7 +6,7 @@ "impact": "critical", "messages": { "pass": "Document has no elements referenced with ARIA or labels that share the same id attribute", - "fail": "Document has multiple elements referenced with ARIA or with the same id attribute: {{=it.data}}" + "fail": "Document has multiple elements referenced with ARIA with the same id attribute: {{=it.data}}" } } } diff --git a/lib/checks/parsing/duplicate-id.json b/lib/checks/parsing/duplicate-id.json index e611b041a6..fd8659b0b7 100644 --- a/lib/checks/parsing/duplicate-id.json +++ b/lib/checks/parsing/duplicate-id.json @@ -5,8 +5,8 @@ "metadata": { "impact": "minor", "messages": { - "pass": "Document has no elements that share the same id attribute", - "fail": "Document has multiple elements with the same id attribute: {{=it.data}}" + "pass": "Document has no static elements that share the same id attribute", + "fail": "Document has multiple static elements with the same id attribute" } } } diff --git a/test/integration/rules/duplicate-id-active/duplicate-id-active.html b/test/integration/rules/duplicate-id-active/duplicate-id-active.html index f82aa81633..63abf5f0bc 100644 --- a/test/integration/rules/duplicate-id-active/duplicate-id-active.html +++ b/test/integration/rules/duplicate-id-active/duplicate-id-active.html @@ -5,6 +5,14 @@ + + + + + + +
+
diff --git a/test/integration/rules/duplicate-id-active/duplicate-id-active.json b/test/integration/rules/duplicate-id-active/duplicate-id-active.json index d9ef605f83..e7e9c6b0f8 100644 --- a/test/integration/rules/duplicate-id-active/duplicate-id-active.json +++ b/test/integration/rules/duplicate-id-active/duplicate-id-active.json @@ -1,6 +1,6 @@ { "description": "duplicate-id-active test", "rule": "duplicate-id-active", - "violations": [[".fail1"]], + "violations": [[".fail1"], [".fail2"], [".fail3"], [".fail4"], [".fail5"]], "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"]] }