From 89d18d08ed72ba944dfce71c72d5887f4ca26cc1 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Wed, 14 Nov 2018 10:27:16 +0100 Subject: [PATCH] feat: Add allowEmpty option for aria-valid-attr-value (#1154) Allow specifying per ARIA attribute which ones are allowed to be empty. Changes include: - aria-haspopup: Now allowed to be empty - aria-invalid: Now allowed to be empty - aria-current: Now allowed to be empty - aria-valuetext: No longer allowed to be empty Closes #994 ## Reviewer checks **Required fields, to be filled out by PR reviewer(s)** - [x] Follows the commit message policy, appropriate for next version - [x] Has documentation updated, a DU ticket, or requires no documentation change - [x] Includes new tests, or was unnecessary - [x] Code is reviewed for security by: Jey --- lib/checks/aria/errormessage.js | 16 +- lib/commons/aria/attributes.js | 69 +-- lib/commons/aria/index.js | 33 +- lib/commons/aria/validate-attr-value.js | 63 +++ test/checks/aria/errormessage.js | 33 ++ test/commons/aria/attributes.js | 294 ------------ test/commons/aria/validate-attr-value.js | 429 ++++++++++++++++++ .../aria-valid-attr-value.html | 14 + .../aria-valid-attr-value.json | 7 +- 9 files changed, 578 insertions(+), 380 deletions(-) create mode 100644 lib/commons/aria/validate-attr-value.js create mode 100644 test/commons/aria/validate-attr-value.js diff --git a/lib/checks/aria/errormessage.js b/lib/checks/aria/errormessage.js index d93113fe82..94b5169c13 100644 --- a/lib/checks/aria/errormessage.js +++ b/lib/checks/aria/errormessage.js @@ -1,12 +1,16 @@ +const { aria, dom } = axe.commons; options = Array.isArray(options) ? options : []; -var attr = node.getAttribute('aria-errormessage'), - hasAttr = node.hasAttribute('aria-errormessage'); +const attr = node.getAttribute('aria-errormessage'); +const hasAttr = node.hasAttribute('aria-errormessage'); -var doc = axe.commons.dom.getRootNode(node); +const doc = dom.getRootNode(node); -function validateAttrValue() { - var idref = attr && doc.getElementById(attr); +function validateAttrValue(attr) { + if (attr.trim() === '') { + return aria.lookupTable.attributes['aria-errormessage'].allowEmpty; + } + const idref = attr && doc.getElementById(attr); if (idref) { return ( idref.getAttribute('role') === 'alert' || @@ -20,7 +24,7 @@ function validateAttrValue() { // limit results to elements that actually have this attribute if (options.indexOf(attr) === -1 && hasAttr) { - if (!validateAttrValue()) { + if (!validateAttrValue(attr)) { this.data(axe.utils.tokenList(attr)); return false; } diff --git a/lib/commons/aria/attributes.js b/lib/commons/aria/attributes.js index 541595f21d..cca4b97517 100644 --- a/lib/commons/aria/attributes.js +++ b/lib/commons/aria/attributes.js @@ -1,4 +1,4 @@ -/* global aria, axe, dom */ +/* global aria, axe */ /** * Get required attributes for a given role @@ -45,70 +45,3 @@ aria.validateAttr = function(att) { 'use strict'; return !!aria.lookupTable.attributes[att]; }; - -/** - * Validate the value of an ARIA attribute - * @method validateAttrValue - * @memberof axe.commons.aria - * @instance - * @param {HTMLElement} node The element to check - * @param {String} attr The name of the attribute - * @return {Boolean} - */ -aria.validateAttrValue = function validateAttrValue(node, attr) { - /*eslint complexity: ["error",17]*/ - 'use strict'; - var matches, - list, - value = node.getAttribute(attr), - attrInfo = aria.lookupTable.attributes[attr]; - - var doc = dom.getRootNode(node); - - if (!attrInfo) { - return true; - } - - switch (attrInfo.type) { - case 'boolean': - case 'nmtoken': - return ( - typeof value === 'string' && - attrInfo.values.includes(value.toLowerCase()) - ); - - case 'nmtokens': - list = axe.utils.tokenList(value); - // Check if any value isn't in the list of values - return list.reduce(function(result, token) { - return result && attrInfo.values.includes(token); - // Initial state, fail if the list is empty - }, list.length !== 0); - - case 'idref': - // idref is allowed to be empty - if (value.trim().length === 0) { - return true; - } - return !!(value && doc.getElementById(value)); - - case 'idrefs': - // idrefs are allowed to be empty - if (value.trim().length === 0) { - return true; - } - list = axe.utils.tokenList(value); - return list.some(token => doc.getElementById(token)); - - case 'string': - // anything goes - return true; - - case 'decimal': - matches = value.match(/^[-+]?([0-9]*)\.?([0-9]*)$/); - return !!(matches && (matches[1] || matches[2])); - - case 'int': - return /^[-+]?[0-9]+$/.test(value); - } -}; diff --git a/lib/commons/aria/index.js b/lib/commons/aria/index.js index 90abf48c8f..eb2cc7f84a 100644 --- a/lib/commons/aria/index.js +++ b/lib/commons/aria/index.js @@ -9,7 +9,8 @@ var aria = (commons.aria = {}), lookupTable.attributes = { 'aria-activedescendant': { - type: 'idref' + type: 'idref', + allowEmpty: true }, 'aria-atomic': { type: 'boolean', @@ -37,14 +38,17 @@ lookupTable.attributes = { type: 'int' }, 'aria-controls': { - type: 'idrefs' + type: 'idrefs', + allowEmpty: true }, 'aria-current': { type: 'nmtoken', + allowEmpty: true, values: ['page', 'step', 'location', 'date', 'time', 'true', 'false'] }, 'aria-describedby': { - type: 'idrefs' + type: 'idrefs', + allowEmpty: true }, 'aria-disabled': { type: 'boolean', @@ -55,14 +59,16 @@ lookupTable.attributes = { values: ['copy', 'move', 'reference', 'execute', 'popup', 'none'] }, 'aria-errormessage': { - type: 'idref' + type: 'idref', + allowEmpty: true }, 'aria-expanded': { type: 'nmtoken', values: ['true', 'false', 'undefined'] }, 'aria-flowto': { - type: 'idrefs' + type: 'idrefs', + allowEmpty: true }, 'aria-grabbed': { type: 'nmtoken', @@ -70,6 +76,7 @@ lookupTable.attributes = { }, 'aria-haspopup': { type: 'nmtoken', + allowEmpty: true, values: ['true', 'false', 'menu', 'listbox', 'tree', 'grid', 'dialog'] }, 'aria-hidden': { @@ -78,16 +85,20 @@ lookupTable.attributes = { }, 'aria-invalid': { type: 'nmtoken', + allowEmpty: true, values: ['true', 'false', 'spelling', 'grammar'] }, 'aria-keyshortcuts': { - type: 'string' + type: 'string', + allowEmpty: true }, 'aria-label': { - type: 'string' + type: 'string', + allowEmpty: true }, 'aria-labelledby': { - type: 'idrefs' + type: 'idrefs', + allowEmpty: true }, 'aria-level': { type: 'int' @@ -113,10 +124,12 @@ lookupTable.attributes = { values: ['horizontal', 'vertical'] }, 'aria-owns': { - type: 'idrefs' + type: 'idrefs', + allowEmpty: true }, 'aria-placeholder': { - type: 'string' + type: 'string', + allowEmpty: true }, 'aria-posinset': { type: 'int' diff --git a/lib/commons/aria/validate-attr-value.js b/lib/commons/aria/validate-attr-value.js new file mode 100644 index 0000000000..bc1d057537 --- /dev/null +++ b/lib/commons/aria/validate-attr-value.js @@ -0,0 +1,63 @@ +/* global aria, dom */ + +/** + * Validate the value of an ARIA attribute + * @method validateAttrValue + * @memberof axe.commons.aria + * @instance + * @param {HTMLElement} node The element to check + * @param {String} attr The name of the attribute + * @return {Boolean} + */ +aria.validateAttrValue = function validateAttrValue(node, attr) { + /*eslint complexity: ["error",17]*/ + 'use strict'; + var matches, + list, + value = node.getAttribute(attr), + attrInfo = aria.lookupTable.attributes[attr]; + + var doc = dom.getRootNode(node); + + if (!attrInfo) { + return true; + } + if (attrInfo.allowEmpty && (!value || value.trim() === '')) { + return true; + } + + switch (attrInfo.type) { + case 'boolean': + case 'nmtoken': + return ( + typeof value === 'string' && + attrInfo.values.includes(value.toLowerCase()) + ); + + case 'nmtokens': + list = axe.utils.tokenList(value); + // Check if any value isn't in the list of values + return list.reduce(function(result, token) { + return result && attrInfo.values.includes(token); + // Initial state, fail if the list is empty + }, list.length !== 0); + + case 'idref': + return !!(value && doc.getElementById(value)); + + case 'idrefs': + list = axe.utils.tokenList(value); + return list.some(token => doc.getElementById(token)); + + case 'string': + // Not allowed empty except with allowEmpty: true + return value.trim() !== ''; + + case 'decimal': + matches = value.match(/^[-+]?([0-9]*)\.?([0-9]*)$/); + return !!(matches && (matches[1] || matches[2])); + + case 'int': + return /^[-+]?[0-9]+$/.test(value); + } +}; diff --git a/test/checks/aria/errormessage.js b/test/checks/aria/errormessage.js index bc2eea55f9..a0e5523845 100644 --- a/test/checks/aria/errormessage.js +++ b/test/checks/aria/errormessage.js @@ -5,8 +5,15 @@ describe('aria-errormessage', function() { var shadowSupported = axe.testUtils.shadowSupport.v1; var shadowCheckSetup = axe.testUtils.shadowCheckSetup; var checkContext = axe.testUtils.MockCheckContext(); + var attrData = Object.assign( + {}, + axe.commons.aria.lookupTable.attributes['aria-errormessage'] + ); afterEach(function() { + axe.commons.aria.lookupTable.attributes[ + 'aria-errormessage' + ] = Object.assign({}, attrData); fixture.innerHTML = ''; checkContext.reset(); }); @@ -66,6 +73,32 @@ describe('aria-errormessage', function() { assert.deepEqual(checkContext._data, ['foo', 'bar', 'baz']); }); + it('returns true when aria-errormessage is empty, if that is allowed', function() { + axe.commons.aria.lookupTable.attributes[ + 'aria-errormessage' + ].allowEmpty = true; + fixture.innerHTML = '
'; + assert.isTrue( + checks['aria-errormessage'].evaluate.call( + checkContext, + fixture.children[0] + ) + ); + }); + + it('returns false when aria-errormessage is empty, if that is not allowed', function() { + axe.commons.aria.lookupTable.attributes[ + 'aria-errormessage' + ].allowEmpty = false; + fixture.innerHTML = '
'; + assert.isFalse( + checks['aria-errormessage'].evaluate.call( + checkContext, + fixture.children[0] + ) + ); + }); + (shadowSupported ? it : xit)( 'should return false if aria-errormessage value crosses shadow boundary', function() { diff --git a/test/commons/aria/attributes.js b/test/commons/aria/attributes.js index b1f8c4fa8e..bbcba380d7 100644 --- a/test/commons/aria/attributes.js +++ b/test/commons/aria/attributes.js @@ -112,297 +112,3 @@ describe('aria.validateAttr', function() { assert.isFalse(axe.commons.aria.validateAttr('cats')); }); }); - -function createContentVAV() { - 'use strict'; - var group = document.createElement('div'); - group.innerHTML = - '' + - '' + - ''; - return group; -} - -function makeShadowTreeVAV(node) { - 'use strict'; - var root = node.attachShadow({ mode: 'open' }); - var div = document.createElement('div'); - div.className = 'parent'; - root.appendChild(div); - div.appendChild(createContentVAV()); -} - -describe('aria.validateAttrValue', function() { - 'use strict'; - - var orig = axe.commons.aria.lookupTable.attributes, - fixture = document.getElementById('fixture'); - - var shadowSupport = axe.testUtils.shadowSupport; - - afterEach(function() { - axe.commons.aria.lookupTable.attributes = orig; - fixture.innerHTML = ''; - }); - - it('should return true if there is no matching attribute (future-compat???)', function() { - var node = document.createElement('div'); - node.setAttribute('cats', 'hello'); - - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - - describe('schema defintions', function() { - describe('enumerated values', function() { - it('should validate against enumerated .values if present', function() { - axe.commons.aria.lookupTable.attributes = { - cats: { - type: 'nmtoken', - values: ['valid'] - } - }; - var node = document.createElement('div'); - node.setAttribute('cats', 'valid'); - - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', 'invalid'); - - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - it('should be case-insensitive for enumerated values', function() { - axe.commons.aria.lookupTable.attributes = { - cats: { - type: 'nmtoken', - values: ['valid'] - } - }; - var node = document.createElement('div'); - node.setAttribute('cats', 'vaLiD'); - - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - it('should reject empty strings', function() { - axe.commons.aria.lookupTable.attributes = { - cats: { - type: 'nmtoken', - values: ['valid'] - } - }; - var node = document.createElement('div'); - node.setAttribute('cats', ''); - - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - }); - describe('idref', function() { - it('should validate the referenced node exists', function() { - axe.commons.aria.lookupTable.attributes = { - cats: { - type: 'idref' - } - }; - - var node = document.createElement('div'); - fixture.innerHTML = '
'; - node.setAttribute('cats', 'target'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', 'invalid'); - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - it('should work in shadow DOM', function() { - var shadEl; - - if (shadowSupport.v1) { - // shadow DOM v1 - note: v0 is compatible with this code, so no need - // to specifically test this - fixture.innerHTML = '
'; - makeShadowTreeVAV(fixture.firstChild); - shadEl = fixture.firstChild.shadowRoot.querySelector('input#myinput'); - assert.isTrue( - axe.commons.aria.validateAttrValue(shadEl, 'aria-labelledby') - ); - shadEl = fixture.firstChild.shadowRoot.querySelector('input#invalid'); - assert.isFalse( - axe.commons.aria.validateAttrValue(shadEl, 'aria-labelledby') - ); - } - }); - }); - describe('idrefs', function() { - var node = document.createElement('div'); - beforeEach(function() { - axe.commons.aria.lookupTable.attributes = { - cats: { - type: 'idrefs' - } - }; - }); - - it('should return false when a single referenced node is not found', function() { - node.setAttribute('cats', 'invalid'); - // target2 not found - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - - it('should return false when at no referenced element is found', function() { - fixture.innerHTML = '
'; - node.setAttribute('cats', 'target2 target3'); - // target2 not found - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - - it('should return true when at least one referenced element is found', function() { - fixture.innerHTML = '
'; - node.setAttribute('cats', 'target target2'); - // target2 not found - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - - it('should return true when all targets are found', function() { - fixture.innerHTML = '
'; - node.setAttribute('cats', 'target target2'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - - it('should not fail on weird whitespace', function() { - fixture.innerHTML = '
'; - node.setAttribute('cats', ' \t \ttarget \t target2 '); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - }); - - describe('string', function() { - it('should always return true', function() { - axe.commons.aria.lookupTable.attributes = { - cats: { - type: 'string' - } - }; - var node = document.createElement('div'); - node.setAttribute('cats', 'hi'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - }); - - describe('decimal', function() { - var node = document.createElement('div'); - beforeEach(function() { - axe.commons.aria.lookupTable.attributes = { - cats: { - type: 'decimal' - } - }; - }); - - it('should allow, but not require, a preceeding sign', function() { - node.setAttribute('cats', '+1.12'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '-1.12'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '1.12'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - - it('should make the decimal separator optional', function() { - node.setAttribute('cats', '+1'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '-1'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '1'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - - it('should make the whole number optional', function() { - node.setAttribute('cats', '+.1'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '-.1'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '.1'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - - it('should make the right-side optional', function() { - node.setAttribute('cats', '+1.'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '-1.'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '1.'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - - it('should validate the entire string', function() { - node.setAttribute('cats', ' +1.12 '); - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', 'invalid +1.12'); - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '+1.12 invalid'); - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - - it('should only allow for numbers', function() { - node.setAttribute('cats', '+a.12'); - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '+1.b'); - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', 'b1.1'); - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - - it('should require at least one number', function() { - node.setAttribute('cats', '+.'); - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '-.'); - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '+'); - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '-'); - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '.'); - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', ''); - assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - }); - - describe('int', function() { - var node = document.createElement('div'); - beforeEach(function() { - axe.commons.aria.lookupTable.attributes = { - cats: { - type: 'int' - } - }; - }); - - it('should only allow for numbers by an optional preceeding sign', function() { - node.setAttribute('cats', '+1234234'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '-137456745'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - - node.setAttribute('cats', '1234523452'); - assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); - }); - }); - }); -}); diff --git a/test/commons/aria/validate-attr-value.js b/test/commons/aria/validate-attr-value.js new file mode 100644 index 0000000000..a01dc5b236 --- /dev/null +++ b/test/commons/aria/validate-attr-value.js @@ -0,0 +1,429 @@ +describe('aria.validateAttrValue', function() { + 'use strict'; + + var orig = axe.commons.aria.lookupTable.attributes, + fixture = document.getElementById('fixture'); + + var shadowSupport = axe.testUtils.shadowSupport; + + afterEach(function() { + axe.commons.aria.lookupTable.attributes = orig; + fixture.innerHTML = ''; + }); + + it('should return true if there is no matching attribute (future-compat???)', function() { + var node = document.createElement('div'); + node.setAttribute('cats', 'hello'); + + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + + it('returns true for empty attributes with allowEmpty:true', function() { + axe.commons.aria.lookupTable.attributes = { + cats: { + type: 'nmtoken', + allowEmpty: true, + values: ['valid'] + }, + dogs: { + type: 'idref', + allowEmpty: true + }, + goats: { + type: 'idrefs', + allowEmpty: true + }, + cows: { + type: 'string', + allowEmpty: true + }, + sheep: { + type: 'decimal', + allowEmpty: true + }, + pigs: { + type: 'int', + allowEmpty: true + } + }; + + var node = document.createElement('div'); + node.setAttribute('cats', ''); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('dogs', ''); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'dogs')); + + node.setAttribute('goats', ''); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'goats')); + + node.setAttribute('sheep', ''); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'sheep')); + + node.setAttribute('cows', ''); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cows')); + + node.setAttribute('pigs', ''); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'pigs')); + }); + it('returns true for whitespace-only attributes with allowEmpty:true', function() { + axe.commons.aria.lookupTable.attributes = { + cats: { + type: 'nmtoken', + allowEmpty: true, + values: ['valid'] + }, + dogs: { + type: 'idref', + allowEmpty: true + }, + goats: { + type: 'idrefs', + allowEmpty: true + }, + cows: { + type: 'string', + allowEmpty: true + }, + sheep: { + type: 'decimal', + allowEmpty: true + }, + pigs: { + type: 'int', + allowEmpty: true + } + }; + + var node = document.createElement('div'); + node.setAttribute('cats', ' \r\n\t '); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('dogs', ' \r\n\t '); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'dogs')); + + node.setAttribute('goats', ' \r\n\t '); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'goats')); + + node.setAttribute('cows', ' \r\n\t '); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cows')); + + node.setAttribute('pigs', ' \r\n\t '); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'sheep')); + + node.setAttribute('sheep', ' \r\n\t '); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'pigs')); + }); + describe('schema defintions', function() { + describe('enumerated values', function() { + it('should validate against enumerated .values if present', function() { + axe.commons.aria.lookupTable.attributes = { + cats: { + type: 'nmtoken', + values: ['valid'] + } + }; + var node = document.createElement('div'); + node.setAttribute('cats', 'valid'); + + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', 'invalid'); + + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + it('should be case-insensitive for enumerated values', function() { + axe.commons.aria.lookupTable.attributes = { + cats: { + type: 'nmtoken', + values: ['valid'] + } + }; + var node = document.createElement('div'); + node.setAttribute('cats', 'vaLiD'); + + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + it('should reject empty strings', function() { + axe.commons.aria.lookupTable.attributes = { + cats: { + type: 'nmtoken', + values: ['valid'] + } + }; + var node = document.createElement('div'); + node.setAttribute('cats', ''); + + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + }); + describe('idref', function() { + it('should validate the referenced node exists', function() { + axe.commons.aria.lookupTable.attributes = { + cats: { + type: 'idref' + } + }; + + var node = document.createElement('div'); + fixture.innerHTML = '
'; + node.setAttribute('cats', 'target'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', 'invalid'); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + it('should work in shadow DOM', function() { + var shadEl; + + if (shadowSupport.v1) { + // shadow DOM v1 - note: v0 is compatible with this code, so no need + // to specifically test this + fixture.innerHTML = '
'; + makeShadowTreeVAV(fixture.firstChild); + shadEl = fixture.firstChild.shadowRoot.querySelector('input#myinput'); + assert.isTrue( + axe.commons.aria.validateAttrValue(shadEl, 'aria-labelledby') + ); + shadEl = fixture.firstChild.shadowRoot.querySelector('input#invalid'); + assert.isFalse( + axe.commons.aria.validateAttrValue(shadEl, 'aria-labelledby') + ); + } + }); + it('returns false if empty without allowEmpty: true', function() { + axe.commons.aria.lookupTable.attributes = { + cats: { + type: 'idref' + } + }; + var node = document.createElement('div'); + node.setAttribute('cats', ''); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + }); + describe('idrefs', function() { + var node = document.createElement('div'); + beforeEach(function() { + axe.commons.aria.lookupTable.attributes = { + cats: { + type: 'idrefs' + } + }; + }); + + it('should return false when a single referenced node is not found', function() { + node.setAttribute('cats', 'invalid'); + // target2 not found + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + + it('should return false when at no referenced element is found', function() { + fixture.innerHTML = '
'; + node.setAttribute('cats', 'target2 target3'); + // target2 not found + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + + it('should return true when at least one referenced element is found', function() { + fixture.innerHTML = '
'; + node.setAttribute('cats', 'target target2'); + // target2 not found + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + + it('should return true when all targets are found', function() { + fixture.innerHTML = '
'; + node.setAttribute('cats', 'target target2'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + + it('should not fail on weird whitespace', function() { + fixture.innerHTML = '
'; + node.setAttribute('cats', ' \t \ttarget \t target2 '); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + + it('returns false if empty without allowEmpty: true', function() { + axe.commons.aria.lookupTable.attributes = { + cats: { + type: 'idrefs' + } + }; + var node = document.createElement('div'); + node.setAttribute('cats', ''); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + }); + + describe('string', function() { + it('returns true for non-empty strings', function() { + axe.commons.aria.lookupTable.attributes = { + cats: { + type: 'string' + } + }; + var node = document.createElement('div'); + node.setAttribute('cats', 'hi'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + it('returns false for non-empty strings without allowEmpty:true', function() { + axe.commons.aria.lookupTable.attributes = { + cats: { + type: 'string' + } + }; + var node = document.createElement('div'); + node.setAttribute('cats', ''); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + }); + + describe('decimal', function() { + var node = document.createElement('div'); + beforeEach(function() { + axe.commons.aria.lookupTable.attributes = { + cats: { + type: 'decimal' + } + }; + }); + + it('should allow, but not require, a preceeding sign', function() { + node.setAttribute('cats', '+1.12'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '-1.12'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '1.12'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + + it('should make the decimal separator optional', function() { + node.setAttribute('cats', '+1'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '-1'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '1'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + + it('should make the whole number optional', function() { + node.setAttribute('cats', '+.1'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '-.1'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '.1'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + + it('should make the right-side optional', function() { + node.setAttribute('cats', '+1.'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '-1.'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '1.'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + + it('should validate the entire string', function() { + node.setAttribute('cats', ' +1.12 '); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', 'invalid +1.12'); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '+1.12 invalid'); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + + it('should only allow for numbers', function() { + node.setAttribute('cats', '+a.12'); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '+1.b'); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', 'b1.1'); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + + it('should require at least one number', function() { + node.setAttribute('cats', '+.'); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '-.'); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '+'); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '-'); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '.'); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', ''); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + + it('returns false for empty strings without allowEmpty:true', function() { + node.setAttribute('cats', ''); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + }); + + describe('int', function() { + var node = document.createElement('div'); + beforeEach(function() { + axe.commons.aria.lookupTable.attributes = { + cats: { + type: 'int' + } + }; + }); + + it('should only allow for numbers by an optional preceeding sign', function() { + node.setAttribute('cats', '+1234234'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '-137456745'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + + node.setAttribute('cats', '1234523452'); + assert.isTrue(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + it('returns false for empty strings without allowEmpty:true', function() { + node.setAttribute('cats', ''); + assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); + }); + }); + }); +}); + +function makeShadowTreeVAV(node) { + 'use strict'; + var root = node.attachShadow({ mode: 'open' }); + var div = document.createElement('div'); + div.className = 'parent'; + root.appendChild(div); + div.appendChild(createContentVAV()); +} + +function createContentVAV() { + 'use strict'; + var group = document.createElement('div'); + group.innerHTML = + '' + + '' + + ''; + return group; +} diff --git a/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.html b/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.html index c4b109cd72..f679c90b96 100644 --- a/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.html +++ b/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.html @@ -40,6 +40,7 @@

Violations

hi
hi
hi
+

Possible False Positives

@@ -250,6 +251,19 @@

Possible False Positives

hi
hi
+
hi
+
hi
+
hi
+
hi
+
hi
+
hi
+
hi
+
hi
+
hi
+
hi
+
hi
+
hi
+
hi
Hi
Hi2
diff --git a/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.json b/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.json index 8b267d5892..42e614f312 100644 --- a/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.json +++ b/test/integration/rules/aria-valid-attr-value/aria-valid-attr-value.json @@ -7,7 +7,8 @@ ["#violation12"], ["#violation13"], ["#violation14"], ["#violation15"], ["#violation16"], ["#violation17"], ["#violation18"], ["#violation19"], ["#violation20"], ["#violation21"], ["#violation22"], ["#violation23"], ["#violation24"], ["#violation25"], ["#violation26"], ["#violation27"], ["#violation28"], ["#violation29"], - ["#violation30"], ["#violation31"], ["#violation32"], ["#violation33"], ["#violation34"],["#violation35"] + ["#violation30"], ["#violation31"], ["#violation32"], ["#violation33"], ["#violation34"], ["#violation35"], + ["#violation36"] ], "passes": [ ["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"], ["#pass5"], ["#pass6"], ["#pass7"], ["#pass8"], ["#pass9"], @@ -32,6 +33,8 @@ ["#pass147"], ["#pass148"], ["#pass149"], ["#pass150"], ["#pass151"], ["#pass152"], ["#pass153"], ["#pass154"], ["#pass155"], ["#pass156"], ["#pass157"], ["#pass158"], ["#pass159"], ["#pass160"], ["#pass161"], ["#pass162"], ["#pass163"], ["#pass164"], ["#pass165"], ["#pass166"], ["#pass167"], - ["#pass168"], ["#pass169"], ["#pass170"], ["#pass171"] + ["#pass168"], ["#pass169"], ["#pass170"], ["#pass171"], ["#pass172"], ["#pass173"], ["#pass174"], + ["#pass175"], ["#pass176"], ["#pass177"], ["#pass178"], ["#pass179"], ["#pass180"], ["#pass181"], + ["#pass182"], ["#pass183"], ["#pass184"] ] }