From 704043e8a4b9359e871403c3b4fc294b9feee931 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Tue, 2 May 2023 15:55:54 +0200 Subject: [PATCH] fix(aria-allowed-attr): no inconsistent aria-checked on HTML checkboxes (#3895) * feat(aria-allowed-attr): no inconsistent aria-checked on HTML checkboxes * Add integration tests * Add non-table row test * Cleanup / docs work * Handle checkbox indeterminate state * Fix virtual rule test * Apply suggestions from code review Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com> * Fix broken test --------- Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com> --- doc/check-options.md | 6 +- lib/checks/aria/aria-allowed-attr-evaluate.js | 62 +--- .../aria/aria-conditional-attr-evaluate.js | 20 ++ lib/checks/aria/aria-conditional-attr.json | 23 ++ ...aria-conditional-checkbox-attr-evaluate.js | 39 +++ .../aria-conditional-row-attr-evaluate.js | 36 +++ lib/core/base/virtual-node/virtual-node.js | 17 +- lib/rules/aria-allowed-attr.json | 4 +- locales/_template.json | 8 + .../{allowed-attr.js => aria-allowed-attr.js} | 118 -------- test/checks/aria/aria-conditional-attr.js | 264 ++++++++++++++++++ .../base/virtual-node/serial-virtual-node.js | 11 + test/core/base/virtual-node/virtual-node.js | 32 +++ .../rules/aria-allowed-attr/failures.html | 4 + .../rules/aria-allowed-attr/failures.json | 5 +- .../rules/aria-allowed-attr/passes.html | 3 + .../rules/aria-allowed-attr/passes.json | 4 +- .../virtual-rules/aria-allowed-attr.js | 15 + 18 files changed, 498 insertions(+), 173 deletions(-) create mode 100644 lib/checks/aria/aria-conditional-attr-evaluate.js create mode 100644 lib/checks/aria/aria-conditional-attr.json create mode 100644 lib/checks/aria/aria-conditional-checkbox-attr-evaluate.js create mode 100644 lib/checks/aria/aria-conditional-row-attr-evaluate.js rename test/checks/aria/{allowed-attr.js => aria-allowed-attr.js} (64%) create mode 100644 test/checks/aria/aria-conditional-attr.js diff --git a/doc/check-options.md b/doc/check-options.md index 87c6adda42..0ca3b53814 100644 --- a/doc/check-options.md +++ b/doc/check-options.md @@ -207,6 +207,10 @@ All checks allow these global options: ### aria-allowed-attr +Previously supported properties `validTreeRowAttrs` is no longer available. `invalidTableRowAttrs` from [aria-conditional-attr](#aria-conditional-attr) instead. + +### aria-conditional-attr + @@ -218,7 +222,7 @@ All checks allow these global options:
- 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 fd028d34e2..20d4246ba6 100644
--- a/locales/_template.json
+++ b/locales/_template.json
@@ -429,6 +429,14 @@
       "pass": "Element has an aria-busy attribute",
       "fail": "Element uses aria-busy=\"true\" while showing a loader"
     },
+    "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 979f75979e..61c0d3f069 100644 --- a/test/integration/rules/aria-allowed-attr/passes.html +++ b/test/integration/rules/aria-allowed-attr/passes.html @@ -2148,3 +2148,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',