diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index db6d5a4c75..88dfac245c 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 | Moderate | 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 | | 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/parsing/duplicate-id-active.json b/lib/checks/parsing/duplicate-id-active.json new file mode 100644 index 0000000000..91c82c6fda --- /dev/null +++ b/lib/checks/parsing/duplicate-id-active.json @@ -0,0 +1,12 @@ +{ + "id": "duplicate-id-active", + "evaluate": "duplicate-id.js", + "after": "duplicate-id-after.js", + "metadata": { + "impact": "serious", + "messages": { + "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/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..ce0915e0d7 --- /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 referenced with ARIA or labels that share the same id attribute", + "fail": "Document has multiple elements referenced with ARIA 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 100% rename from lib/checks/shared/duplicate-id.js rename to lib/checks/parsing/duplicate-id.js diff --git a/lib/checks/parsing/duplicate-id.json b/lib/checks/parsing/duplicate-id.json new file mode 100644 index 0000000000..fd8659b0b7 --- /dev/null +++ b/lib/checks/parsing/duplicate-id.json @@ -0,0 +1,12 @@ +{ + "id": "duplicate-id", + "evaluate": "duplicate-id.js", + "after": "duplicate-id-after.js", + "metadata": { + "impact": "minor", + "messages": { + "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/lib/checks/shared/duplicate-id.json b/lib/checks/shared/duplicate-id.json deleted file mode 100644 index 3d55e3fb1f..0000000000 --- a/lib/checks/shared/duplicate-id.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "id": "duplicate-id", - "evaluate": "duplicate-id.js", - "after": "duplicate-id-after.js", - "metadata": { - "impact": "moderate", - "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..f743fd798a --- /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 (let i = 0; i < node.children.length; i++) { + const out = findDomNode(node.children[i], 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-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..219ce13dc4 --- /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 of active elements is unique", + "help": "IDs of active elements must be unique" + }, + "all": [], + "any": [ + "duplicate-id-active" + ], + "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..6300d94aa7 --- /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": [], + "any": [ + "duplicate-id-aria" + ], + "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 215238238f..fd788dd41e 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", diff --git a/test/checks/shared/duplicate-id.js b/test/checks/parser/duplicate-id.js similarity index 100% rename from test/checks/shared/duplicate-id.js rename to test/checks/parser/duplicate-id.js diff --git a/test/commons/aria/is-accessible-ref.js b/test/commons/aria/is-accessible-ref.js new file mode 100644 index 0000000000..4d926cf710 --- /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.v1; + + 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)); + } + ); +}); 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..63abf5f0bc --- /dev/null +++ b/test/integration/rules/duplicate-id-active/duplicate-id-active.html @@ -0,0 +1,27 @@ +

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..e7e9c6b0f8 --- /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"], [".fail2"], [".fail3"], [".fail4"], [".fail5"]], + "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)); + }); +});