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 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('hello world ');
+ 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(
+ 'hello world '
+ );
+ 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"]
]
}