diff --git a/lib/checks/color/color-contrast-evaluate.js b/lib/checks/color/color-contrast-evaluate.js index eabf6aa536..18a58f4e4b 100644 --- a/lib/checks/color/color-contrast-evaluate.js +++ b/lib/checks/color/color-contrast-evaluate.js @@ -41,7 +41,10 @@ export default function colorContrastEvaluate(node, options, virtualNode) { // if element or a parent has pseudo content then we need to mark // as needs review - const pseudoElm = findPseudoElement(virtualNode, { ignorePseudo, pseudoSizeThreshold }) + const pseudoElm = findPseudoElement(virtualNode, { + ignorePseudo, + pseudoSizeThreshold + }); if (pseudoElm) { this.data({ messageKey: 'pseudoContent' }); this.relatedNodes(pseudoElm.actualNode); @@ -103,7 +106,7 @@ export default function colorContrastEvaluate(node, options, virtualNode) { let missing; if (bgColor === null) { missing = incompleteData.get('bgColor'); - } else { + } else if (!isValid) { missing = contrastContributor; } @@ -111,7 +114,7 @@ export default function colorContrastEvaluate(node, options, virtualNode) { const shortTextContent = visibleText.length === 1; if (equalRatio) { missing = incompleteData.set('bgColor', 'equalRatio'); - } else if (shortTextContent && !ignoreLength) { + } else if (!isValid && shortTextContent && !ignoreLength) { // Check that the text content is a single character long missing = 'shortTextContent'; } @@ -148,27 +151,31 @@ export default function colorContrastEvaluate(node, options, virtualNode) { return isValid; } -function findPseudoElement(vNode, { - pseudoSizeThreshold = 0.25, - ignorePseudo = false -}) { +function findPseudoElement( + vNode, + { pseudoSizeThreshold = 0.25, ignorePseudo = false } +) { if (ignorePseudo) { return; } const rect = vNode.boundingClientRect; const minimumSize = rect.width * rect.height * pseudoSizeThreshold; do { - const beforeSize = getPseudoElementArea(vNode.actualNode, ':before') - const afterSize = getPseudoElementArea(vNode.actualNode, ':after') + const beforeSize = getPseudoElementArea(vNode.actualNode, ':before'); + const afterSize = getPseudoElementArea(vNode.actualNode, ':after'); if (beforeSize + afterSize > minimumSize) { - return vNode // Combined area of before and after exceeds the minimum size + return vNode; // Combined area of before and after exceeds the minimum size } - } while (vNode = vNode.parent); + } while ((vNode = vNode.parent)); } -const getPseudoElementArea = memoize(function getPseudoElementArea(node, pseudo) { +const getPseudoElementArea = memoize(function getPseudoElementArea( + node, + pseudo +) { const style = window.getComputedStyle(node, pseudo); - const matchPseudoStyle = (prop, value) => style.getPropertyValue(prop) === value; + const matchPseudoStyle = (prop, value) => + style.getPropertyValue(prop) === value; if ( matchPseudoStyle('content', 'none') || matchPseudoStyle('display', 'none') || @@ -190,9 +197,7 @@ const getPseudoElementArea = memoize(function getPseudoElementArea(node, pseudo) const pseudoHeight = parseUnit(style.getPropertyValue('height')); if (pseudoWidth.unit !== 'px' || pseudoHeight.unit !== 'px') { // IE doesn't normalize to px. Infinity gets everything to undefined - return (pseudoWidth.value === 0 || pseudoHeight.value === 0 - ? 0 : Infinity - ); + return pseudoWidth.value === 0 || pseudoHeight.value === 0 ? 0 : Infinity; } return pseudoWidth.value * pseudoHeight.value; }); @@ -200,8 +205,9 @@ const getPseudoElementArea = memoize(function getPseudoElementArea(node, pseudo) function textIsEmojis(visibleText) { const options = { nonBmp: true }; const hasUnicodeChars = hasUnicode(visibleText, options); - const hasNonUnicodeChars = sanitize(removeUnicode(visibleText, options)) === '' - return hasUnicodeChars && hasNonUnicodeChars + const hasNonUnicodeChars = + sanitize(removeUnicode(visibleText, options)) === ''; + return hasUnicodeChars && hasNonUnicodeChars; } function parseUnit(str) { diff --git a/test/checks/shared/non-empty-if-present.js b/test/checks/shared/non-empty-if-present.js index 48b772b4cd..ee0bf6ef36 100644 --- a/test/checks/shared/non-empty-if-present.js +++ b/test/checks/shared/non-empty-if-present.js @@ -23,7 +23,7 @@ describe('non-empty-if-present', function() { assert.isFalse( axe.testUtils - .getCheckEvaluate('non-empty-if-present') + .getCheckEvaluate('non-empty-if-present', { verifyMessage: false }) .call(checkContext, null, {}, vNode) ); assert.equal(checkContext._data.messageKey, 'has-label'); diff --git a/test/testutils.js b/test/testutils.js index 8c60d13a93..fdc5a8741d 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -25,6 +25,30 @@ if (!fixture) { document.body.insertBefore(fixture, document.body.firstChild); } +// determine which checks are used only in the `none` array of rules +var noneChecks = []; + +function verifyIsNoneCheck(check) { + var index = noneChecks.indexOf(check); + if (index !== -1) { + noneChecks.splice(index, 1); + } +} + +axe._audit.rules.forEach(function(rule) { + rule.none.forEach(function(check) { + check = check.id || check; + if (noneChecks.indexOf(check) === -1) { + noneChecks.push(check); + } + }); +}); + +axe._audit.rules.forEach(function(rule) { + rule.any.forEach(verifyIsNoneCheck); + rule.all.forEach(verifyIsNoneCheck); +}); + /** * Create a check context for mocking/resetting data and relatedNodes in tests * @@ -408,11 +432,61 @@ testUtils.queryFixture = function queryFixture(html, query) { * @param {String} checkId - ID of the check * @return Function */ -testUtils.getCheckEvaluate = function getCheckEvaluate(checkId) { +testUtils.getCheckEvaluate = function getCheckEvaluate(checkId, testOptions) { var check = checks[checkId]; + testOptions = testOptions || {}; + return function evaluateWrapper(node, options, virtualNode, context) { var opts = check.getOptions(options); - return check.evaluate.call(this, node, opts, virtualNode, context); + var result = check.evaluate.call(this, node, opts, virtualNode, context); + + // ensure that every result has a corresponding message + if (testOptions.verifyMessage !== false) { + var messages = axe._audit.data.checks[checkId].messages; + var messageKey = this._data && this._data.messageKey; + + // see how the check is used to know where to find the message + // e.g. a check used only in the `none` array of a rule will look at + // the messageKey of a passing result in the `fail` messages + var keyResult = result; + var isNoneCheck = noneChecks.indexOf(checkId) !== -1; + if (isNoneCheck) { + keyResult = result === true ? false : result === false ? true : result; + } + + var key = + keyResult === true + ? 'pass' + : keyResult === false + ? 'fail' + : 'incomplete'; + var noneCheckMessage = isNoneCheck + ? '. Note that since this check is only used in the "none" array of all rules, the messages use the inverse of the result (e.g. a result of false uses the "pass" messages)' + : ''; + + assert.exists( + messages[key], + 'Missing "' + + key + + '" message for check result of ' + + result + + noneCheckMessage + ); + if (messageKey) { + assert.exists( + messages[key][messageKey], + 'Missing ' + + key + + ' message key "' + + messageKey + + '" for check result of ' + + result + + noneCheckMessage + ); + } + } + + return result; }; };