diff --git a/lib/commons/aria/get-element-unallowed-roles.js b/lib/commons/aria/get-element-unallowed-roles.js index 9209443353..5b285857db 100644 --- a/lib/commons/aria/get-element-unallowed-roles.js +++ b/lib/commons/aria/get-element-unallowed-roles.js @@ -98,7 +98,6 @@ function getElementUnallowedRoles(node, allowImplicit = true) { ) { return true; } - // check if role is allowed on element return !isAriaRoleAllowedOnElement(vNode, role); }); diff --git a/lib/commons/matches/from-definition.js b/lib/commons/matches/from-definition.js index 14564b99e5..7afefcac13 100644 --- a/lib/commons/matches/from-definition.js +++ b/lib/commons/matches/from-definition.js @@ -1,3 +1,4 @@ +import hasAccessibleName from './has-accessible-name'; import attributes from './attributes'; import condition from './condition'; import explicitRole from './explicit-role'; @@ -9,6 +10,7 @@ import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-n import { getNodeFromTree, matches } from '../../core/utils'; const matchers = { + hasAccessibleName, attributes, condition, explicitRole, diff --git a/lib/commons/matches/has-accessible-name.js b/lib/commons/matches/has-accessible-name.js new file mode 100644 index 0000000000..7a3cad27d0 --- /dev/null +++ b/lib/commons/matches/has-accessible-name.js @@ -0,0 +1,25 @@ +import accessibleTextVirtual from '../text/accessible-text-virtual'; +import fromPrimative from './from-primative'; + +/** + * Check if a virtual node has a non-empty accessible name + *`` + * Note: matches.hasAccessibleName(vNode, true) can be indirectly used through + * matches(vNode, { hasAccessibleName: boolean }) + * + * Example: + * ```js + * matches.hasAccessibleName(vNode, true); + * matches.hasAccessibleName(vNode, false); + * + * ``` + * + * @param {VirtualNode} vNode + * @param {Object} matcher + * @returns {Boolean} + */ +function hasAccessibleName(vNode, matcher) { + return fromPrimative(!!accessibleTextVirtual(vNode), matcher); +} + +export default hasAccessibleName; diff --git a/lib/commons/matches/index.js b/lib/commons/matches/index.js index f0434676fc..025381d5c4 100644 --- a/lib/commons/matches/index.js +++ b/lib/commons/matches/index.js @@ -3,6 +3,7 @@ * @namespace commons.matches * @memberof axe */ +import hasAccessibleName from './has-accessible-name'; import attributes from './attributes'; import condition from './condition'; import explicitRole from './explicit-role'; @@ -15,6 +16,7 @@ import nodeName from './node-name'; import properties from './properties'; import semanticRole from './semantic-role'; +matches.hasAccessibleName = hasAccessibleName; matches.attributes = attributes; matches.condition = condition; matches.explicitRole = explicitRole; diff --git a/lib/commons/standards/get-element-spec.js b/lib/commons/standards/get-element-spec.js index 10354dad56..218fb30fd6 100644 --- a/lib/commons/standards/get-element-spec.js +++ b/lib/commons/standards/get-element-spec.js @@ -6,7 +6,7 @@ import matchesFn from '../../commons/matches'; * @param {VirtualNode} vNode The VirtualNode to get the spec for. * @return {Object} The standard spec object */ -function getElementSpec(vNode) { +function getElementSpec(vNode, { noMatchAccessibleName = false } = {}) { const standard = standards.htmlElms[vNode.props.nodeName]; // invalid element name (could be an svg or custom element name) @@ -29,6 +29,13 @@ function getElementSpec(vNode) { } const { matches, ...props } = variant[variantName]; + const matchProperties = Array.isArray(matches) ? matches : [matches]; + for (let i = 0; i < matchProperties.length && noMatchAccessibleName; i++) { + if (matchProperties[i].hasOwnProperty('hasAccessibleName')) { + return standard; + } + } + if (matchesFn(vNode, matches)) { for (const propName in props) { if (props.hasOwnProperty(propName)) { diff --git a/lib/commons/text/native-text-alternative.js b/lib/commons/text/native-text-alternative.js index 3fd64e1c68..ff2b12ad08 100644 --- a/lib/commons/text/native-text-alternative.js +++ b/lib/commons/text/native-text-alternative.js @@ -37,7 +37,7 @@ function nativeTextAlternative(virtualNode, context = {}) { * @return {Function[]} Array of native accessible name computation methods */ function findTextMethods(virtualNode) { - const elmSpec = getElementSpec(virtualNode); + const elmSpec = getElementSpec(virtualNode, { noMatchAccessibleName: true }); const methods = elmSpec.namingMethods || []; return methods.map(methodName => nativeTextMethods[methodName]); diff --git a/lib/commons/text/subtree-text.js b/lib/commons/text/subtree-text.js index 2e8b1c4395..a3d669f342 100644 --- a/lib/commons/text/subtree-text.js +++ b/lib/commons/text/subtree-text.js @@ -2,7 +2,7 @@ import accessibleTextVirtual from './accessible-text-virtual'; import namedFromContents from '../aria/named-from-contents'; import getOwnedVirtual from '../aria/get-owned-virtual'; import getElementsByContentType from '../standards/get-elements-by-content-type'; -import getElementSpec from '../standards/get-element-spec' +import getElementSpec from '../standards/get-element-spec'; /** * Get the accessible text for an element that can get its name from content @@ -16,7 +16,9 @@ function subtreeText(virtualNode, context = {}) { const { alreadyProcessed } = accessibleTextVirtual; context.startNode = context.startNode || virtualNode; const { strict, inControlContext, inLabelledByContext } = context; - const { contentTypes } = getElementSpec(virtualNode); + const { contentTypes } = getElementSpec(virtualNode, { + noMatchAccessibleName: true + }); if ( alreadyProcessed(virtualNode, context) || virtualNode.props.nodeType !== 1 || diff --git a/lib/standards/html-elms.js b/lib/standards/html-elms.js index a583f553f6..62aa89d45b 100644 --- a/lib/standards/html-elms.js +++ b/lib/standards/html-elms.js @@ -330,11 +330,17 @@ const htmlElms = { img: { variant: { nonEmptyAlt: { - matches: { - attributes: { - alt: '/.+/' + matches: [ + { + // Because foo has no accessible name: + attributes: { + alt: '/.+/' + } + }, + { + hasAccessibleName: true } - }, + ], allowedRoles: [ 'button', 'checkbox', diff --git a/test/checks/aria/aria-allowed-role.js b/test/checks/aria/aria-allowed-role.js index 4231feec89..0c78f505f7 100644 --- a/test/checks/aria/aria-allowed-role.js +++ b/test/checks/aria/aria-allowed-role.js @@ -135,6 +135,82 @@ describe('aria-allowed-role', function() { assert.deepEqual(checkContext._data, ['none']); }); + it('returns true when img has aria-label and a valid role, role="button"', function() { + var vNode = queryFixture( + '' + ); + assert.isTrue( + axe.testUtils + .getCheckEvaluate('aria-allowed-role') + .call(checkContext, null, null, vNode) + ); + assert.isNull(checkContext._data, null); + }); + + it('returns false when img has aria-label and a invalid role, role="alert"', function() { + var vNode = queryFixture( + '' + ); + assert.isFalse( + axe.testUtils + .getCheckEvaluate('aria-allowed-role') + .call(checkContext, null, null, vNode) + ); + assert.deepEqual(checkContext._data, ['alert']); + }); + + it('returns true when img has aria-labelledby and a valid role, role="menuitem"', function() { + var vNode = queryFixture( + '
hello world
' + + '' + ); + assert.isTrue( + axe.testUtils + .getCheckEvaluate('aria-allowed-role') + .call(checkContext, null, null, vNode) + ); + assert.isNull(checkContext._data, null); + }); + + it('returns false when img has aria-labelledby and a invalid role, role="rowgroup"', function() { + var vNode = queryFixture( + '
hello world
' + + '' + ); + assert.isFalse( + axe.testUtils + .getCheckEvaluate('aria-allowed-role') + .call(checkContext, null, null, vNode) + ); + assert.deepEqual(checkContext._data, ['rowgroup']); + }); + + it('returns true when img has title and a valid role, role="link"', function() { + var vNode = queryFixture( + '
hello world
' + + '' + ); + assert.isTrue( + axe.testUtils + .getCheckEvaluate('aria-allowed-role') + .call(checkContext, null, null, vNode) + ); + assert.isNull(checkContext._data, null); + }); + + it('returns false when img has title and a invalid role, role="radiogroup"', function() { + var vNode = queryFixture( + '
hello world
' + + '' + ); + assert.isFalse( + axe.testUtils + .getCheckEvaluate('aria-allowed-role') + .call(checkContext, null, null, vNode) + ); + assert.deepEqual(checkContext._data, ['radiogroup']); + }); + it('returns true when input of type image and no role', function() { var vNode = queryFixture(''); assert.isTrue( diff --git a/test/checks/aria/valid-attr-value.js b/test/checks/aria/valid-attr-value.js index 8327223131..41a49b9e1f 100644 --- a/test/checks/aria/valid-attr-value.js +++ b/test/checks/aria/valid-attr-value.js @@ -234,6 +234,30 @@ describe('aria-valid-attr-value', function() { ); }); + it('should return true on valid aria-labelledby value within img elm', function() { + var vNode = queryFixture( + '
hello world
' + + '' + ); + assert.isTrue( + axe.testUtils + .getCheckEvaluate('aria-valid-attr-value') + .call(checkContext, null, null, vNode) + ); + }); + + it('should return undefined on invalid aria-labelledby value within img elm', function() { + var vNode = queryFixture( + '
hello world
' + + '' + ); + assert.isUndefined( + axe.testUtils + .getCheckEvaluate('aria-valid-attr-value') + .call(checkContext, null, null, vNode) + ); + }); + describe('options', function() { it('should exclude supplied attributes', function() { var vNode = queryFixture( diff --git a/test/commons/matches/from-definition.js b/test/commons/matches/from-definition.js index 4c0f198934..a0f4b7f034 100644 --- a/test/commons/matches/from-definition.js +++ b/test/commons/matches/from-definition.js @@ -165,6 +165,20 @@ describe('matches.fromDefinition', function() { ); }); + it('matches a definition with an `accessibleName` property', function() { + var virtualNode = queryFixture(''); + assert.isTrue( + fromDefinition(virtualNode, { + hasAccessibleName: true + }) + ); + assert.isFalse( + fromDefinition(virtualNode, { + hasAccessibleName: false + }) + ); + }); + it('returns true when all matching properties return true', function() { var virtualNode = queryFixture( '' diff --git a/test/commons/matches/has-accessible-name.js b/test/commons/matches/has-accessible-name.js new file mode 100644 index 0000000000..684f399d63 --- /dev/null +++ b/test/commons/matches/has-accessible-name.js @@ -0,0 +1,96 @@ +describe('matches.accessibleName', function() { + var hasAccessibleName = axe.commons.matches.hasAccessibleName; + var fixture = document.querySelector('#fixture'); + var queryFixture = axe.testUtils.queryFixture; + + beforeEach(function() { + fixture.innerHTML = ''; + }); + + it('should return true when text has an accessible name', function() { + var virtualNode = queryFixture(''); + assert.isTrue(hasAccessibleName(virtualNode, true)); + }); + + it('should return true when aria-label has an accessible name', function() { + var virtualNode = queryFixture( + '' + ); + assert.isTrue(hasAccessibleName(virtualNode, true)); + }); + + it('should return true when aria-labelledby has an accessible name', function() { + var virtualNode = queryFixture( + '
hello world' + ); + assert.isTrue(hasAccessibleName(virtualNode, true)); + }); + + it('should return true when label has an accessible name', function() { + var virtualNode = queryFixture( + '' + ); + assert.isTrue(hasAccessibleName(virtualNode, true)); + }); + + it('should return false when text does not have an accessible name', function() { + var virtualNode = queryFixture(''); + assert.isFalse(hasAccessibleName(virtualNode, true)); + }); + + it('should return false when aria-label does not have an accessible name', function() { + var virtualNode = queryFixture( + '' + ); + assert.isFalse(hasAccessibleName(virtualNode, true)); + }); + + it('should return false when aria-labelledby does not have an accessible name', function() { + var virtualNode = queryFixture( + '
hello world
hello world
hello world'); + assert.isFalse(hasAccessibleName(virtualNode, true)); + }); + + it('should return false when label does not have an accessible name', function() { + var virtualNode = queryFixture(''); + assert.isFalse(hasAccessibleName(virtualNode, true)); + }); + + it('works with SerialVirtualNode', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'button', + attributes: { + role: 'button', + 'aria-label': 'hello world' + } + }); + assert.isTrue(hasAccessibleName(serialNode, true)); + }); +}); diff --git a/test/integration/rules/aria-allowed-role/aria-allowed-role.html b/test/integration/rules/aria-allowed-role/aria-allowed-role.html index ca7aec245f..7c4402682a 100644 --- a/test/integration/rules/aria-allowed-role/aria-allowed-role.html +++ b/test/integration/rules/aria-allowed-role/aria-allowed-role.html @@ -219,6 +219,23 @@

ok
ok
+ + +
hazaar
+ + + + + +
ok
ok
diff --git a/test/integration/rules/aria-allowed-role/aria-allowed-role.json b/test/integration/rules/aria-allowed-role/aria-allowed-role.json index 33c694ee33..d37b335f5c 100644 --- a/test/integration/rules/aria-allowed-role/aria-allowed-role.json +++ b/test/integration/rules/aria-allowed-role/aria-allowed-role.json @@ -72,7 +72,10 @@ ["#p-text"], ["#pass-graphics-document"], ["#pass-graphics-object"], - ["#pass-graphics-symbol"] + ["#pass-graphics-symbol"], + ["#pass-img-valid-role-aria-label"], + ["#pass-img-valid-role-title"], + ["#pass-img-valid-role-aria-labelledby"] ], "violations": [ ["#fail-dd-no-role"], @@ -98,7 +101,11 @@ ["#fail-text-1"], ["#fail-text-2"], ["#fail-text-3"], - ["#fail-text-4"] + ["#fail-text-4"], + ["#fail-img-invalid-role-aria-label"], + ["#fail-img-invalid-role-title"], + ["#fail-img-invalid-role-aria-labelledby"], + ["#fail-img-no-accessible-name-present"] ], "incomplete": [["#incomplete1"], ["#incomplete2"]] } diff --git a/test/integration/rules/image-alt/image-alt.html b/test/integration/rules/image-alt/image-alt.html index b1226b92ba..a34644a736 100644 --- a/test/integration/rules/image-alt/image-alt.html +++ b/test/integration/rules/image-alt/image-alt.html @@ -23,3 +23,13 @@ + + + + + + + + + + diff --git a/test/integration/rules/image-alt/image-alt.json b/test/integration/rules/image-alt/image-alt.json index d55d112a77..418e9f6137 100644 --- a/test/integration/rules/image-alt/image-alt.json +++ b/test/integration/rules/image-alt/image-alt.json @@ -13,7 +13,11 @@ ["#violation9"], ["#violation10"], ["#violation11"], - ["#violation12"] + ["#violation12"], + ["#violation13"], + ["#violation14"], + ["#violation15"], + ["#violation16"] ], "passes": [ ["#pass1"], @@ -23,6 +27,9 @@ ["#pass5"], ["#pass6"], ["#pass7"], - ["#pass8"] + ["#pass8"], + ["#pass9"], + ["#pass10"], + ["#pass11"] ] }