diff --git a/lib/checks/aria/aria-allowed-attr-evaluate.js b/lib/checks/aria/aria-allowed-attr-evaluate.js index 9d309437bf..c72f7bb70a 100644 --- a/lib/checks/aria/aria-allowed-attr-evaluate.js +++ b/lib/checks/aria/aria-allowed-attr-evaluate.js @@ -1,5 +1,6 @@ -import { uniqueArray, closest } from '../../core/utils'; +import { uniqueArray, closest, isHtmlElement } from '../../core/utils'; import { getRole, allowedAttr, validateAttr } from '../../commons/aria'; +import { isFocusable } from '../../commons/dom'; import cache from '../../core/base/cache'; /** @@ -69,7 +70,7 @@ function ariaAllowedAttrEvaluate(node, options, virtualNode) { ariaAttr.forEach(attr => { preChecks[attr] = validateRowAttrs; }); - if (role && allowed) { + if (allowed) { for (let i = 0; i < attrs.length; i++) { const attrName = attrs[i]; if (validateAttr(attrName) && preChecks[attrName]?.()) { @@ -82,6 +83,11 @@ function ariaAllowedAttrEvaluate(node, options, virtualNode) { if (invalid.length) { this.data(invalid); + + if (!isHtmlElement(virtualNode) && !role && !isFocusable(virtualNode)) { + return undefined; + } + return false; } diff --git a/lib/checks/aria/aria-allowed-attr.json b/lib/checks/aria/aria-allowed-attr.json index 4079b3ebcb..cf39faf10f 100644 --- a/lib/checks/aria/aria-allowed-attr.json +++ b/lib/checks/aria/aria-allowed-attr.json @@ -16,7 +16,8 @@ "fail": { "singular": "ARIA attribute is not allowed: ${data.values}", "plural": "ARIA attributes are not allowed: ${data.values}" - } + }, + "incomplete": "Check that there is no problem if the ARIA attribute is ignored on this element: ${data.values}" } } } diff --git a/test/aria-practices/apg.spec.js b/test/aria-practices/apg.spec.js index bc50ba64b1..524568d9d2 100644 --- a/test/aria-practices/apg.spec.js +++ b/test/aria-practices/apg.spec.js @@ -6,11 +6,13 @@ const { getWebdriver, connectToChromeDriver } = require('./run-server'); const { assert } = require('chai'); const globby = require('globby'); -describe('aria-practices', function () { +describe('aria-practices', function() { // Use path.resolve rather than require.resolve because APG has no package.json const apgPath = path.resolve(__dirname, '../../node_modules/aria-practices/'); - const filePaths = globby.sync(`${apgPath}/examples/**/*.html`) - const testFiles = filePaths.map(fileName => fileName.split('/aria-practices/examples/')[1]) + const filePaths = globby.sync(`${apgPath}/examples/**/*.html`); + const testFiles = filePaths.map( + fileName => fileName.split('/aria-practices/examples/')[1] + ); const port = 9515; const addr = `http://localhost:9876/node_modules/aria-practices/`; let driver, axeSource; @@ -36,22 +38,24 @@ describe('aria-practices', function () { 'color-contrast', 'heading-order', // w3c/aria-practices#2119 'list', // w3c/aria-practices#2118 - 'scrollable-region-focusable', // w3c/aria-practices#2114 + 'scrollable-region-focusable' // w3c/aria-practices#2114 ], 'feed/feedDisplay.html': ['page-has-heading-one'], // w3c/aria-practices#2120 // "page within a page" type thing going on 'menubar/menubar-navigation.html': [ 'aria-allowed-role', 'landmark-banner-is-top-level', - 'landmark-contentinfo-is-top-level', + 'landmark-contentinfo-is-top-level' ], // "page within a page" type thing going on 'treeview/treeview-navigation.html': [ 'aria-allowed-role', 'landmark-banner-is-top-level', 'landmark-contentinfo-is-top-level' - ] - } + ], + //https://github.com/w3c/aria-practices/issues/2199 + 'button/button_idl.html': ['aria-allowed-attr'] + }; // Not an actual content file const skippedPages = [ @@ -60,19 +64,24 @@ describe('aria-practices', function () { 'toolbar/help.html' // Embedded into another page ]; - testFiles.filter(filePath => !skippedPages.includes(filePath)).forEach(filePath => { - it(`finds no issue in "${filePath}"`, async () => { - await driver.get(`${addr}/examples/${filePath}`); - - const builder = new AxeBuilder(driver, axeSource); - builder.disableRules([ - ...disabledRules['*'], - ...(disabledRules[filePath] || []), - ]); - - const { violations } = await builder.analyze(); - const issues = violations.map(({ id, nodes }) => ({ id, issues: nodes.length })) - assert.lengthOf(issues, 0); + testFiles + .filter(filePath => !skippedPages.includes(filePath)) + .forEach(filePath => { + it(`finds no issue in "${filePath}"`, async () => { + await driver.get(`${addr}/examples/${filePath}`); + + const builder = new AxeBuilder(driver, axeSource); + builder.disableRules([ + ...disabledRules['*'], + ...(disabledRules[filePath] || []) + ]); + + const { violations } = await builder.analyze(); + const issues = violations.map(({ id, nodes }) => ({ + id, + issues: nodes.length + })); + assert.lengthOf(issues, 0); + }); }); - }); }); diff --git a/test/checks/aria/allowed-attr.js b/test/checks/aria/allowed-attr.js index dc36f5fde9..177e7bda8c 100644 --- a/test/checks/aria/allowed-attr.js +++ b/test/checks/aria/allowed-attr.js @@ -59,7 +59,7 @@ describe('aria-allowed-attr', function() { assert.isNull(checkContext._data); }); - it.skip('should return false for non-global attributes if there is no role', function() { + it('should return false for non-global attributes if there is no role', function() { var vNode = queryFixture( '
' ); @@ -72,9 +72,9 @@ describe('aria-allowed-attr', function() { assert.deepEqual(checkContext._data, ['aria-selected="true"']); }); - it('should return true for non-global attributes if there is no role', function() { + it('should not report on invalid attributes', function() { var vNode = queryFixture( - '
' + '' ); assert.isTrue( @@ -82,11 +82,12 @@ describe('aria-allowed-attr', function() { .getCheckEvaluate('aria-allowed-attr') .call(checkContext, null, null, vNode) ); + assert.isNull(checkContext._data); }); - it('should not report on invalid attributes', function() { + it('should not report on allowed attributes', function() { var vNode = queryFixture( - '' + '' ); assert.isTrue( @@ -97,18 +98,45 @@ describe('aria-allowed-attr', function() { assert.isNull(checkContext._data); }); - it('should not report on allowed attributes', function() { + it('should return undefined for custom element that has no role and is not focusable', function() { var vNode = queryFixture( - '' + '' ); - assert.isTrue( + assert.isUndefined( axe.testUtils .getCheckEvaluate('aria-allowed-attr') .call(checkContext, null, null, vNode) ); - assert.isNull(checkContext._data); + assert.isNotNull(checkContext._data); }); + + it("should return false for custom element that has a role which doesn't allow the attribute", function() { + var vNode = queryFixture( + '' + ); + + assert.isFalse( + axe.testUtils + .getCheckEvaluate('aria-allowed-attr') + .call(checkContext, null, null, vNode) + ); + assert.isNotNull(checkContext._data); + }); + + it('should return false for custom element that is focusable', function() { + var vNode = queryFixture( + '' + ); + + assert.isFalse( + axe.testUtils + .getCheckEvaluate('aria-allowed-attr') + .call(checkContext, null, null, vNode) + ); + assert.isNotNull(checkContext._data); + }); + describe('invalid aria-attributes when used on role=row as a descendant of a table or a grid', function() { [ 'aria-posinset="1"', diff --git a/test/integration/rules/aria-allowed-attr/failures.html b/test/integration/rules/aria-allowed-attr/failures.html index ab576ae3ff..578b339141 100644 --- a/test/integration/rules/aria-allowed-attr/failures.html +++ b/test/integration/rules/aria-allowed-attr/failures.html @@ -28,12 +28,12 @@ - +>
fail
fail
fail
diff --git a/test/integration/rules/aria-allowed-attr/failures.json b/test/integration/rules/aria-allowed-attr/failures.json index da4956aa20..ccedf0b10f 100644 --- a/test/integration/rules/aria-allowed-attr/failures.json +++ b/test/integration/rules/aria-allowed-attr/failures.json @@ -31,6 +31,7 @@ ["#fail27"], ["#fail28"], ["#fail29"], + ["#fail30"], ["#fail31"], ["#fail32"], ["#fail33"], diff --git a/test/integration/rules/aria-allowed-attr/incomplete.html b/test/integration/rules/aria-allowed-attr/incomplete.html index 35793fb4a2..7391d58120 100644 --- a/test/integration/rules/aria-allowed-attr/incomplete.html +++ b/test/integration/rules/aria-allowed-attr/incomplete.html @@ -1,2 +1,3 @@
Foo
Foo
+Foo diff --git a/test/integration/rules/aria-allowed-attr/incomplete.json b/test/integration/rules/aria-allowed-attr/incomplete.json index 475f884bdd..1d8c7e3fd7 100644 --- a/test/integration/rules/aria-allowed-attr/incomplete.json +++ b/test/integration/rules/aria-allowed-attr/incomplete.json @@ -1,5 +1,5 @@ { "description": "aria-allowed-attr incomplete tests", "rule": "aria-allowed-attr", - "incomplete": [["#incomplete0"], ["#incomplete1"]] + "incomplete": [["#incomplete0"], ["#incomplete1"], ["#incomplete2"]] } diff --git a/test/integration/virtual-rules/aria-allowed-attr.js b/test/integration/virtual-rules/aria-allowed-attr.js index ee42253088..b53d188397 100644 --- a/test/integration/virtual-rules/aria-allowed-attr.js +++ b/test/integration/virtual-rules/aria-allowed-attr.js @@ -55,7 +55,7 @@ describe('aria-allowed-attr virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it.skip('should fail for non-global attributes and element with no role', function() { + it('should fail for non-global attributes and element with no role', function() { var results = axe.runVirtualRule('aria-allowed-attr', { nodeName: 'div', attributes: { @@ -119,4 +119,17 @@ describe('aria-allowed-attr virtual-rule', function() { assert.lengthOf(results.violations, 1); assert.lengthOf(results.incomplete, 0); }); + + it('should incomplete for non-global attributes and custom element', function() { + var results = axe.runVirtualRule('aria-allowed-attr', { + nodeName: 'custom-elm1', + attributes: { + 'aria-checked': true + } + }); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 1); + }); });