Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(aria-allowed-attr): check for invalid aria-attributes for role="row" #3160

Merged
merged 16 commits into from
Oct 1, 2021
44 changes: 40 additions & 4 deletions lib/checks/aria/aria-allowed-attr-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { uniqueArray } from '../../core/utils';
import { uniqueArray, closest } from '../../core/utils';
import { getRole, allowedAttr, validateAttr } from '../../commons/aria';
import cache from '../../core/base/cache';

/**
* Check if each ARIA attribute on an element is allowed for its semantic role.
Expand Down Expand Up @@ -27,21 +28,56 @@ import { getRole, allowedAttr, validateAttr } from '../../commons/aria';
*/
function ariaAllowedAttrEvaluate(node, options, virtualNode) {
const invalid = [];

const role = getRole(virtualNode);
const attrs = virtualNode.attrNames;
let allowed = allowedAttr(role);

// @deprecated: allowed attr options to pass more attrs.
// configure the standards spec instead
if (Array.isArray(options[role])) {
allowed = uniqueArray(options[role].concat(allowed));
}

// TODO: look into memoizing getRole and removing this local cache
let tableMap = cache.get('aria-allowed-attr-table');
if (!tableMap) {
tableMap = new WeakMap();
cache.set('aria-allowed-attr-table', tableMap);
}

function validateRowAttrs() {
// check if the parent exists otherwise a TypeError will occur (virtual-nodes specifically)
if (virtualNode.parent && role === 'row') {
const table = closest(
virtualNode,
'table, [role="treegrid"], [role="table"], [role="grid"]'
);

let tableRole = tableMap.get(table);
if (table && !tableRole) {
tableRole = getRole(table);
tableMap.set(table, tableRole);
}
if (['table', 'grid'].includes(tableRole) && role === 'row') {
return true;
}
}
}
// Allows options to be mapped to object e.g. {'aria-level' : validateRowAttrs}
const preChecks = Object.assign(
{},
...options.ariaAttr.map(key => ({ [key]: validateRowAttrs }))
);

if (allowed) {
for (let i = 0; i < attrs.length; i++) {
const attrName = attrs[i];
if (validateAttr(attrName) && !allowed.includes(attrName)) {
if (
validateAttr(attrName) &&
attrName in preChecks &&
preChecks[attrName]()
) {
invalid.push(attrName + '="' + virtualNode.attr(attrName) + '"');
} else if (validateAttr(attrName) && !allowed.includes(attrName)) {
invalid.push(attrName + '="' + virtualNode.attr(attrName) + '"');
}
}
Expand Down
3 changes: 3 additions & 0 deletions lib/checks/aria/aria-allowed-attr.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"id": "aria-allowed-attr",
"evaluate": "aria-allowed-attr-evaluate",
"options": {
"ariaAttr": ["aria-posinset", "aria-setsize", "aria-expanded", "aria-level"]
},
"metadata": {
"impact": "critical",
"messages": {
Expand Down
1 change: 0 additions & 1 deletion lib/checks/aria/aria-allowed-role-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ function ariaAllowedRoleEvaluate(node, options = {}, virtualNode) {
}

const unallowedRoles = getElementUnallowedRoles(virtualNode, allowImplicit);

if (unallowedRoles.length) {
this.data(unallowedRoles);
if (!isVisible(virtualNode, true)) {
Expand Down
87 changes: 87 additions & 0 deletions test/checks/aria/allowed-attr.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,93 @@ describe('aria-allowed-attr', function() {
);
assert.isNull(checkContext._data);
});
describe('should return false if aria-attributes are invalid for table or a grid', function() {
[
'aria-posinset="1"',
'aria-setsize="1"',
'aria-expanded="true"',
'aria-level="1"'
].forEach(function(attrName) {
it(
'should return false when ' +
attrName +
' is used on role=row thats parent is a table',
function() {
var vNode = queryFixture(
' <div role="table">' +
'<div id="target" role="row" ' +
attrName +
'></div>' +
'</div>'
);
assert.isFalse(
axe.testUtils
.getCheckEvaluate('aria-allowed-attr')
.call(checkContext, null, null, vNode)
);
assert.isNotNull(checkContext._data);
}
);
});

[
'aria-posinset="1"',
'aria-setsize="1"',
'aria-expanded="true"',
'aria-level="1"'
].forEach(function(attrName) {
it(
'should return false when ' +
attrName +
' is used on role=row thats parent is a grid',
function() {
var vNode = queryFixture(
' <div role="grid">' +
'<div id="target" role="row" ' +
attrName +
'></div>' +
'</div>'
);
assert.isFalse(
axe.testUtils
.getCheckEvaluate('aria-allowed-attr')
.call(checkContext, null, null, vNode)
);
assert.isNotNull(checkContext._data);
}
);
});

it('should return false when provided a single aria-attribute is provided ', function() {
axe.configure({
checks: [
{
id: 'aria-allowed-attr',
options: {
ariaAttr: ['aria-posinset']
}
}
]
});

var options = {
ariaAttr: ['aria-posinset']
};
var vNode = queryFixture(
' <div role="table">' +
'<div id="target" role="row" aria-posinset="2"><div role="cell"></div></div>' +
'<div id="row23" role="row" aria-setsize="8"><div role="cell"></div></div>' +
'<div id="row24" role="row" aria-level="3"><div role="cell"></div></div>' +
'</div>'
);

assert.isFalse(
axe.testUtils
.getCheckEvaluate('aria-allowed-attr')
.call(checkContext, null, options, vNode)
);
});
});

describe('options', function() {
it('should allow provided attribute names for a role', function() {
Expand Down
14 changes: 14 additions & 0 deletions test/integration/rules/aria-allowed-attr/failures.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,17 @@
<div role="mark" aria-labelledby="value" id="fail32">fail</div>
<div role="suggestion" aria-label="value" id="fail33">fail</div>
<div role="suggestion" aria-labelledby="value" id="fail34">fail</div>

<div role="table">
<div role="row" aria-expanded="false" id="fail35"></div>
<div role="row" aria-posinset="1" id="fail36"></div>
<div role="row" aria-setsize="10" id="fail37"></div>
<div role="row" aria-level="1" id="fail38"></div>
</div>

<div role="grid">
<div role="row" aria-expanded="false" id="fail39"></div>
<div role="row" aria-posinset="1" id="fail40"></div>
<div role="row" aria-setsize="10" id="fail41"></div>
<div role="row" aria-level="1" id="fail42"></div>
</div>
10 changes: 9 additions & 1 deletion test/integration/rules/aria-allowed-attr/failures.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@
["#fail31"],
["#fail32"],
["#fail33"],
["#fail34"]
["#fail34"],
["#fail35"],
["#fail36"],
["#fail37"],
["#fail38"],
["#fail39"],
["#fail40"],
["#fail41"],
["#fail42"]
]
}