- validTreeRowAttrs
+ invalidTableRowAttrs
|
[
diff --git a/lib/checks/aria/aria-allowed-attr-evaluate.js b/lib/checks/aria/aria-allowed-attr-evaluate.js
index efd1913938..a1049ef38d 100644
--- a/lib/checks/aria/aria-allowed-attr-evaluate.js
+++ b/lib/checks/aria/aria-allowed-attr-evaluate.js
@@ -1,7 +1,6 @@
-import { uniqueArray, closest, isHtmlElement } from '../../core/utils';
+import { uniqueArray, isHtmlElement } from '../../core/utils';
import { getRole, allowedAttr, validateAttr } from '../../commons/aria';
import { isFocusable } from '../../commons/dom';
-import cache from '../../core/base/cache';
/**
* Check if each ARIA attribute on an element is allowed for its semantic role.
@@ -30,62 +29,31 @@ import cache from '../../core/base/cache';
export default 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));
}
- const tableMap = cache.get('aria-allowed-attr-table', () => new WeakMap());
-
- 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 ariaAttr = Array.isArray(options.validTreeRowAttrs)
- ? options.validTreeRowAttrs
- : [];
- const preChecks = {};
- ariaAttr.forEach(attr => {
- preChecks[attr] = validateRowAttrs;
- });
- if (allowed) {
- for (let i = 0; i < attrs.length; i++) {
- const attrName = attrs[i];
- if (validateAttr(attrName) && preChecks[attrName]?.()) {
- invalid.push(attrName + '="' + virtualNode.attr(attrName) + '"');
- } else if (validateAttr(attrName) && !allowed.includes(attrName)) {
- invalid.push(attrName + '="' + virtualNode.attr(attrName) + '"');
- }
+ // Unknown ARIA attributes are tested in aria-valid-attr
+ for (const attrName of virtualNode.attrNames) {
+ if (validateAttr(attrName) && !allowed.includes(attrName)) {
+ invalid.push(attrName);
}
}
- if (invalid.length) {
- this.data(invalid);
+ if (!invalid.length) {
+ return true;
+ }
- if (!isHtmlElement(virtualNode) && !role && !isFocusable(virtualNode)) {
- return undefined;
- }
+ this.data(
+ invalid.map(attrName => attrName + '="' + virtualNode.attr(attrName) + '"')
+ );
- return false;
+ if (!role && !isHtmlElement(virtualNode) && !isFocusable(virtualNode)) {
+ return undefined;
}
-
- return true;
+ return false;
}
diff --git a/lib/checks/aria/aria-conditional-attr-evaluate.js b/lib/checks/aria/aria-conditional-attr-evaluate.js
new file mode 100644
index 0000000000..92fca67525
--- /dev/null
+++ b/lib/checks/aria/aria-conditional-attr-evaluate.js
@@ -0,0 +1,20 @@
+import getRole from '../../commons/aria/get-role';
+import ariaConditionalCheckboxAttr from './aria-conditional-checkbox-attr-evaluate';
+import ariaConditionalRowAttr from './aria-conditional-row-attr-evaluate';
+
+const conditionalRoleMap = {
+ row: ariaConditionalRowAttr,
+ checkbox: ariaConditionalCheckboxAttr
+};
+
+export default function ariaConditionalAttrEvaluate(
+ node,
+ options,
+ virtualNode
+) {
+ const role = getRole(virtualNode);
+ if (!conditionalRoleMap[role]) {
+ return true;
+ }
+ return conditionalRoleMap[role].call(this, node, options, virtualNode);
+}
diff --git a/lib/checks/aria/aria-conditional-attr.json b/lib/checks/aria/aria-conditional-attr.json
new file mode 100644
index 0000000000..c55829016c
--- /dev/null
+++ b/lib/checks/aria/aria-conditional-attr.json
@@ -0,0 +1,23 @@
+{
+ "id": "aria-conditional-attr",
+ "evaluate": "aria-conditional-attr-evaluate",
+ "options": {
+ "invalidTableRowAttrs": [
+ "aria-posinset",
+ "aria-setsize",
+ "aria-expanded",
+ "aria-level"
+ ]
+ },
+ "metadata": {
+ "impact": "serious",
+ "messages": {
+ "pass": "ARIA attribute is allowed",
+ "fail": {
+ "checkbox": "Remove aria-checked, or set it to \"${data.checkState}\" to match the real checkbox state",
+ "rowSingular": "This attribute is supported with treegrid rows, but not ${data.ownerRole}: ${data.invalidAttrs}",
+ "rowPlural": "These attributes are supported with treegrid rows, but not ${data.ownerRole}: ${data.invalidAttrs}"
+ }
+ }
+ }
+}
diff --git a/lib/checks/aria/aria-conditional-checkbox-attr-evaluate.js b/lib/checks/aria/aria-conditional-checkbox-attr-evaluate.js
new file mode 100644
index 0000000000..66c44dcca2
--- /dev/null
+++ b/lib/checks/aria/aria-conditional-checkbox-attr-evaluate.js
@@ -0,0 +1,39 @@
+export default function ariaConditionalCheckboxAttr(
+ node,
+ options,
+ virtualNode
+) {
+ const { nodeName, type } = virtualNode.props;
+ const ariaChecked = normalizeAriaChecked(virtualNode.attr('aria-checked'));
+ if (nodeName !== 'input' || type !== 'checkbox' || !ariaChecked) {
+ return true;
+ }
+
+ const checkState = getCheckState(virtualNode);
+ if (ariaChecked === checkState) {
+ return true;
+ }
+ this.data({
+ messageKey: 'checkbox',
+ checkState
+ });
+ return false;
+}
+
+function getCheckState(vNode) {
+ if (vNode.props.indeterminate) {
+ return 'mixed';
+ }
+ return vNode.props.checked ? 'true' : 'false';
+}
+
+function normalizeAriaChecked(ariaCheckedVal) {
+ if (!ariaCheckedVal) {
+ return '';
+ }
+ ariaCheckedVal = ariaCheckedVal.toLowerCase();
+ if (['mixed', 'true'].includes(ariaCheckedVal)) {
+ return ariaCheckedVal;
+ }
+ return 'false';
+}
diff --git a/lib/checks/aria/aria-conditional-row-attr-evaluate.js b/lib/checks/aria/aria-conditional-row-attr-evaluate.js
new file mode 100644
index 0000000000..3ece49dec0
--- /dev/null
+++ b/lib/checks/aria/aria-conditional-row-attr-evaluate.js
@@ -0,0 +1,36 @@
+import getRole from '../../commons/aria/get-role';
+import { closest } from '../../core/utils';
+
+export default function ariaConditionalRowAttr(
+ node,
+ { invalidTableRowAttrs } = {},
+ virtualNode
+) {
+ const invalidAttrs =
+ invalidTableRowAttrs?.filter?.(invalidAttr => {
+ return virtualNode.hasAttr(invalidAttr);
+ }) ?? [];
+ if (invalidAttrs.length === 0) {
+ return true;
+ }
+
+ const owner = getRowOwner(virtualNode);
+ const ownerRole = owner && getRole(owner);
+ if (!ownerRole || ownerRole === 'treegrid') {
+ return true;
+ }
+
+ const messageKey = `row${invalidAttrs.length > 1 ? 'Plural' : 'Singular'}`;
+ this.data({ messageKey, invalidAttrs, ownerRole });
+ return false;
+}
+
+function getRowOwner(virtualNode) {
+ // check if the parent exists otherwise a TypeError will occur (virtual-nodes specifically)
+ if (!virtualNode.parent) {
+ return;
+ }
+ const rowOwnerQuery =
+ 'table:not([role]), [role~="treegrid"], [role~="table"], [role~="grid"]';
+ return closest(virtualNode, rowOwnerQuery);
+}
diff --git a/lib/core/base/virtual-node/virtual-node.js b/lib/core/base/virtual-node/virtual-node.js
index 0c5e2d2a92..e752e07040 100644
--- a/lib/core/base/virtual-node/virtual-node.js
+++ b/lib/core/base/virtual-node/virtual-node.js
@@ -56,8 +56,17 @@ class VirtualNode extends AbstractVirtualNode {
// add to the prototype so memory is shared across all virtual nodes
get props() {
if (!this._cache.hasOwnProperty('props')) {
- const { nodeType, nodeName, id, multiple, nodeValue, value, selected } =
- this.actualNode;
+ const {
+ nodeType,
+ nodeName,
+ id,
+ multiple,
+ nodeValue,
+ value,
+ selected,
+ checked,
+ indeterminate
+ } = this.actualNode;
this._cache.props = {
nodeType,
@@ -67,7 +76,9 @@ class VirtualNode extends AbstractVirtualNode {
multiple,
nodeValue,
value,
- selected
+ selected,
+ checked,
+ indeterminate
};
}
diff --git a/lib/rules/aria-allowed-attr.json b/lib/rules/aria-allowed-attr.json
index 19f705bf85..90cf8f3a19 100644
--- a/lib/rules/aria-allowed-attr.json
+++ b/lib/rules/aria-allowed-attr.json
@@ -7,7 +7,7 @@
"description": "Ensures ARIA attributes are allowed for an element's role",
"help": "Elements must only use allowed ARIA attributes"
},
- "all": [],
- "any": ["aria-allowed-attr"],
+ "all": ["aria-allowed-attr", "aria-conditional-attr"],
+ "any": [],
"none": ["aria-unsupported-attr", "aria-prohibited-attr"]
}
diff --git a/locales/_template.json b/locales/_template.json
index 54ae98ef55..06425f1fe9 100644
--- a/locales/_template.json
+++ b/locales/_template.json
@@ -429,6 +429,14 @@
"pass": "Element has an aria-busy attribute",
"fail": "Element has no aria-busy=\"true\" attribute"
},
+ "aria-conditional-attr": {
+ "pass": "ARIA attribute is allowed",
+ "fail": {
+ "checkbox": "Remove aria-checked, or set it to \"${data.checkState}\" to match the real checkbox state",
+ "rowSingular": "This attribute is supported with treegrid rows, but not ${data.ownerRole}: ${data.invalidAttrs}",
+ "rowPlural": "These attributes are supported with treegrid rows, but not ${data.ownerRole}: ${data.invalidAttrs}"
+ }
+ },
"aria-errormessage": {
"pass": "aria-errormessage exists and references elements visible to screen readers that use a supported aria-errormessage technique",
"fail": {
diff --git a/test/checks/aria/allowed-attr.js b/test/checks/aria/aria-allowed-attr.js
similarity index 64%
rename from test/checks/aria/allowed-attr.js
rename to test/checks/aria/aria-allowed-attr.js
index 4725c4645f..b1a0d5933f 100644
--- a/test/checks/aria/allowed-attr.js
+++ b/test/checks/aria/aria-allowed-attr.js
@@ -137,124 +137,6 @@ describe('aria-allowed-attr', function () {
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"',
- '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(
- ' '
- );
- 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(
- ' '
- );
- assert.isFalse(
- axe.testUtils
- .getCheckEvaluate('aria-allowed-attr')
- .call(checkContext, null, null, vNode)
- );
- assert.isNotNull(checkContext._data);
- }
- );
- });
- });
-
- describe('options.invalidRowAttrs on role=row when a descendant of a table or a grid', function () {
- it('should return false when provided a single aria-attribute is provided for a table', function () {
- axe.configure({
- checks: [
- {
- id: 'aria-allowed-attr',
- options: {
- validTreeRowAttrs: ['aria-posinset']
- }
- }
- ]
- });
-
- var options = {
- validTreeRowAttrs: ['aria-posinset']
- };
- var vNode = queryFixture(
- ' '
- );
-
- assert.isFalse(
- axe.testUtils
- .getCheckEvaluate('aria-allowed-attr')
- .call(checkContext, null, options, vNode)
- );
- assert.isNotNull(checkContext._data);
- });
-
- it('should return false when provided a single aria-attribute is provided for a grid', function () {
- axe.configure({
- checks: [
- {
- id: 'aria-allowed-attr',
- options: {
- validTreeRowAttrs: ['aria-level']
- }
- }
- ]
- });
-
- var options = {
- validTreeRowAttrs: ['aria-level']
- };
- var vNode = queryFixture(
- ' '
- );
-
- assert.isFalse(
- axe.testUtils
- .getCheckEvaluate('aria-allowed-attr')
- .call(checkContext, null, options, vNode)
- );
- assert.isNotNull(checkContext._data);
- });
- });
-
describe('options', function () {
it('should allow provided attribute names for a role', function () {
axe.configure({
diff --git a/test/checks/aria/aria-conditional-attr.js b/test/checks/aria/aria-conditional-attr.js
new file mode 100644
index 0000000000..2a3f6dc4e6
--- /dev/null
+++ b/test/checks/aria/aria-conditional-attr.js
@@ -0,0 +1,264 @@
+describe('aria-conditional-attr', () => {
+ const { checkSetup, getCheckEvaluate } = axe.testUtils;
+ const checkContext = axe.testUtils.MockCheckContext();
+ const ariaConditionalCheck = getCheckEvaluate('aria-conditional-attr');
+
+ afterEach(() => {
+ checkContext.reset();
+ });
+
+ it('is true for non-conditional roles', () => {
+ const roles = ['main', 'button', 'radiogroup', 'tree', 'none'];
+ for (const role of roles) {
+ const params = checkSetup(``);
+ assert.isTrue(ariaConditionalCheck.apply(checkContext, params));
+ }
+ });
+
+ describe('ariaConditionalRoleAttr', () => {
+ const treeGridRowProps = [
+ 'aria-posinset="1"',
+ 'aria-setsize="1"',
+ 'aria-expanded="true"',
+ 'aria-level="1"'
+ ];
+
+ it('returns true when valid ARIA props are used on table', () => {
+ const params = checkSetup(
+ ``
+ );
+ assert.isTrue(ariaConditionalCheck.apply(checkContext, params));
+ assert.isNull(checkContext._data);
+ });
+
+ it('returns true when treegrid row props are used on a treegrid row', () => {
+ const params = checkSetup(
+ ``
+ );
+ assert.isTrue(ariaConditionalCheck.apply(checkContext, params));
+ assert.isNull(checkContext._data);
+ });
+
+ it('returns true when the row is not in a table, grid, or treegrid', () => {
+ const params = checkSetup(
+ ``
+ );
+ assert.isTrue(ariaConditionalCheck.apply(checkContext, params));
+ assert.isNull(checkContext._data);
+ });
+
+ it('returns false when treegrid row props are used on an ARIA table row', () => {
+ for (const prop of treeGridRowProps) {
+ const params = checkSetup(
+ ``
+ );
+ assert.isFalse(ariaConditionalCheck.apply(checkContext, params));
+ assert.deepEqual(checkContext._data, {
+ messageKey: 'rowSingular',
+ invalidAttrs: [prop.split('=')[0]],
+ ownerRole: 'table'
+ });
+ }
+ });
+
+ it('returns false when treegrid row props are used on a grid row', () => {
+ for (const prop of treeGridRowProps) {
+ const params = checkSetup(
+ ``
+ );
+ assert.isFalse(ariaConditionalCheck.apply(checkContext, params));
+ assert.deepEqual(checkContext._data, {
+ messageKey: 'rowSingular',
+ invalidAttrs: [prop.split('=')[0]],
+ ownerRole: 'grid'
+ });
+ }
+ });
+
+ it('returns false when treegrid row props are used on a native table row', () => {
+ for (const prop of treeGridRowProps) {
+ const params = checkSetup(
+ ``
+ );
+ assert.isFalse(ariaConditionalCheck.apply(checkContext, params));
+ assert.deepEqual(checkContext._data, {
+ messageKey: 'rowSingular',
+ invalidAttrs: [prop.split('=')[0]],
+ ownerRole: 'table'
+ });
+ }
+ });
+
+ it('sets messageKey to rowPlural with multiple bad attributes', () => {
+ const params = checkSetup(
+ ``
+ );
+ assert.isFalse(ariaConditionalCheck.apply(checkContext, params));
+ assert.deepEqual(checkContext._data, {
+ messageKey: 'rowPlural',
+ invalidAttrs: ['aria-expanded', 'aria-level'],
+ ownerRole: 'table'
+ });
+ });
+
+ describe('options.invalidTableRowAttrs', function () {
+ it('returns false for removed attribute', () => {
+ const options = { invalidTableRowAttrs: ['aria-rowindex'] };
+ const params = checkSetup(
+ ``,
+ options
+ );
+ assert.isFalse(ariaConditionalCheck.apply(checkContext, params));
+ });
+
+ it('returns true for additional attribute', () => {
+ const options = { invalidTableRowAttrs: ['aria-level'] };
+ const params = checkSetup(
+ ``,
+ options
+ );
+ assert.isTrue(ariaConditionalCheck.apply(checkContext, params));
+ });
+ });
+ });
+
+ describe('ariaConditionalCheckboxAttr', () => {
+ it('returns true for non-native checkbox', () => {
+ const params = checkSetup(
+ ``
+ );
+ assert.isTrue(ariaConditionalCheck.apply(checkContext, params));
+ assert.isNull(checkContext._data);
+ });
+
+ it('returns true for checkbox without aria-checked value', () => {
+ for (const prop of ['', 'aria-checked', 'aria-checked=""']) {
+ const params = checkSetup(
+ ``
+ );
+ assert.isTrue(ariaConditionalCheck.apply(checkContext, params));
+ assert.isNull(checkContext._data);
+ }
+ });
+
+ describe('checked state', () => {
+ const fixture = document.querySelector('#fixture');
+
+ it('returns true for aria-checked="true" on a [checked] checkbox', () => {
+ fixture.innerHTML = ``;
+ const root = axe.setup(fixture);
+ const vNode = axe.utils.querySelectorAll(root, 'input')[0];
+
+ assert.isTrue(
+ ariaConditionalCheck.call(checkContext, null, null, vNode)
+ );
+ assert.isNull(checkContext._data);
+ });
+
+ it('returns true for aria-checked="true" on a clicked checkbox', () => {
+ fixture.innerHTML = ``;
+ fixture.firstChild.click(); // set checked state
+ const root = axe.setup(fixture);
+ const vNode = axe.utils.querySelectorAll(root, 'input')[0];
+
+ assert.isTrue(
+ ariaConditionalCheck.call(checkContext, null, null, vNode)
+ );
+ assert.isNull(checkContext._data);
+ });
+
+ it('returns false for other aria-checked values', () => {
+ for (const prop of [' ', 'false', 'mixed', 'incorrect', ' true ']) {
+ const params = checkSetup(
+ ``
+ );
+ assert.isFalse(ariaConditionalCheck.apply(checkContext, params));
+ assert.deepEqual(checkContext._data, {
+ messageKey: 'checkbox',
+ checkState: 'true'
+ });
+ }
+ });
+ });
+
+ describe('unchecked state', () => {
+ it('returns true for aria-checked="false"', () => {
+ const params = checkSetup(
+ ``
+ );
+ assert.isTrue(ariaConditionalCheck.apply(checkContext, params));
+ assert.isNull(checkContext._data);
+ });
+
+ it('returns true for aria-checked with an invalid value', () => {
+ for (const prop of [' ', 'invalid', 'FALSE', 'nope']) {
+ const params = checkSetup(
+ ``
+ );
+ assert.isTrue(ariaConditionalCheck.apply(checkContext, params));
+ assert.isNull(checkContext._data);
+ }
+ });
+
+ it('returns false for other aria-checked values', () => {
+ for (const prop of ['true', 'TRUE', 'mixed', 'MiXeD']) {
+ const params = checkSetup(
+ ``
+ );
+ assert.isFalse(ariaConditionalCheck.apply(checkContext, params));
+ assert.deepEqual(checkContext._data, {
+ messageKey: 'checkbox',
+ checkState: 'false'
+ });
+ }
+ });
+ });
+
+ describe('indeterminate state', () => {
+ function asIndeterminateVirtualNode(html) {
+ const fixture = document.querySelector('#fixture');
+ fixture.innerHTML = html;
+ fixture.querySelector('input').indeterminate = true;
+ const root = axe.setup(fixture);
+ return axe.utils.querySelectorAll(root, 'input')[0];
+ }
+
+ it('returns true for aria-checked="mixed"', () => {
+ const vNode = asIndeterminateVirtualNode(
+ ``
+ );
+ assert.isTrue(
+ ariaConditionalCheck.call(checkContext, null, null, vNode)
+ );
+ });
+
+ it('returns false for other aria-checked values', () => {
+ for (const prop of ['true', 'TRUE', 'false', 'invalid']) {
+ const vNode = asIndeterminateVirtualNode(
+ ``
+ );
+ assert.isFalse(
+ ariaConditionalCheck.call(checkContext, null, null, vNode)
+ );
+ assert.deepEqual(checkContext._data, {
+ messageKey: 'checkbox',
+ checkState: 'mixed'
+ });
+ axe.teardown(); // Reset for the next iteration
+ }
+ });
+ });
+ });
+});
diff --git a/test/core/base/virtual-node/serial-virtual-node.js b/test/core/base/virtual-node/serial-virtual-node.js
index 04e0f9af16..81736df6af 100644
--- a/test/core/base/virtual-node/serial-virtual-node.js
+++ b/test/core/base/virtual-node/serial-virtual-node.js
@@ -187,6 +187,17 @@ describe('SerialVirtualNode', function () {
});
assert.equal(vNode.props.type, 'month');
});
+
+ it('reflects checkbox properties', () => {
+ var vNode = new SerialVirtualNode({
+ nodeName: 'input',
+ type: 'checkbox',
+ checked: true,
+ indeterminate: true
+ });
+ assert.equal(vNode.props.checked, true);
+ assert.equal(vNode.props.indeterminate, true);
+ });
});
describe('attr', function () {
diff --git a/test/core/base/virtual-node/virtual-node.js b/test/core/base/virtual-node/virtual-node.js
index 5206df3e2f..73e6998320 100644
--- a/test/core/base/virtual-node/virtual-node.js
+++ b/test/core/base/virtual-node/virtual-node.js
@@ -188,6 +188,38 @@ describe('VirtualNode', function () {
});
});
+ describe('checkbox properties', () => {
+ it('should reflect the checked property', function () {
+ const div = document.createElement('div');
+ const vDiv = new VirtualNode(div);
+ assert.isUndefined(vDiv.props.checked);
+
+ const node = document.createElement('input');
+ node.setAttribute('type', 'checkbox');
+ const vUnchecked = new VirtualNode(node);
+ assert.isFalse(vUnchecked.props.checked);
+
+ node.click();
+ const vChecked = new VirtualNode(node);
+ assert.equal(vChecked.props.checked, true);
+ });
+
+ it('reflects the indeterminate property', () => {
+ const div = document.createElement('div');
+ const vDiv = new VirtualNode(div);
+ assert.isUndefined(vDiv.props.indeterminate);
+
+ const node = document.createElement('input');
+ node.setAttribute('type', 'checkbox');
+ const vUnchecked = new VirtualNode(node);
+ assert.isFalse(vUnchecked.props.indeterminate);
+
+ node.indeterminate = true;
+ const vIndeterminate = new VirtualNode(node);
+ assert.isTrue(vIndeterminate.props.indeterminate);
+ });
+ });
+
describe.skip('isFocusable', function () {
var commons;
diff --git a/test/integration/rules/aria-allowed-attr/failures.html b/test/integration/rules/aria-allowed-attr/failures.html
index c12fd3c7e0..cef9d931ff 100644
--- a/test/integration/rules/aria-allowed-attr/failures.html
+++ b/test/integration/rules/aria-allowed-attr/failures.html
@@ -52,3 +52,7 @@
+
+
+
+
diff --git a/test/integration/rules/aria-allowed-attr/failures.json b/test/integration/rules/aria-allowed-attr/failures.json
index ccedf0b10f..895d49aac7 100644
--- a/test/integration/rules/aria-allowed-attr/failures.json
+++ b/test/integration/rules/aria-allowed-attr/failures.json
@@ -43,6 +43,9 @@
["#fail39"],
["#fail40"],
["#fail41"],
- ["#fail42"]
+ ["#fail42"],
+ ["#fail43"],
+ ["#fail44"],
+ ["#fail45"]
]
}
diff --git a/test/integration/rules/aria-allowed-attr/passes.html b/test/integration/rules/aria-allowed-attr/passes.html
index fe562556bc..6d5d224de5 100644
--- a/test/integration/rules/aria-allowed-attr/passes.html
+++ b/test/integration/rules/aria-allowed-attr/passes.html
@@ -1945,3 +1945,6 @@
>
ok
+
+
+
diff --git a/test/integration/rules/aria-allowed-attr/passes.json b/test/integration/rules/aria-allowed-attr/passes.json
index 769cb1f931..84f487f8b1 100644
--- a/test/integration/rules/aria-allowed-attr/passes.json
+++ b/test/integration/rules/aria-allowed-attr/passes.json
@@ -94,6 +94,8 @@
["#pass89"],
["#pass90"],
["#treegrid"],
- ["#pass91"]
+ ["#pass91"],
+ ["#pass92"],
+ ["#pass93"]
]
}
diff --git a/test/integration/virtual-rules/aria-allowed-attr.js b/test/integration/virtual-rules/aria-allowed-attr.js
index 4f9b67a2f4..9c89aa9e74 100644
--- a/test/integration/virtual-rules/aria-allowed-attr.js
+++ b/test/integration/virtual-rules/aria-allowed-attr.js
@@ -120,6 +120,21 @@ describe('aria-allowed-attr virtual-rule', function () {
assert.lengthOf(results.incomplete, 0);
});
+ it('fails when aria-checked is inconsistent with native checkbox state', () => {
+ var results = axe.runVirtualRule('aria-allowed-attr', {
+ nodeName: 'input',
+ checked: true,
+ attributes: {
+ type: 'checkbox',
+ 'aria-checked': 'false'
+ }
+ });
+
+ assert.lengthOf(results.passes, 0);
+ 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',
|