diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 4eec3529ca..8c27b475ea 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -41,6 +41,7 @@ | image-alt | Ensures <img> elements have alternate text or a role of none or presentation | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a | true | | image-redundant-alt | Ensure button and link text is not repeated as image alternative | Minor | cat.text-alternatives, best-practice | true | | input-image-alt | Ensures <input type="image"> elements have alternate text | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a | true | +| label-content-name-mismatch | Ensures that elements labelled through their content must have their visible text as part of their accessible name | Serious | wcag21a, wcag253, experimental | true | | label-title-only | Ensures that every form element is not solely labeled using the title or aria-describedby attributes | Serious | cat.forms, best-practice | true | | label | Ensures every form element has a label | Minor, Critical | cat.forms, wcag2a, wcag332, wcag131, section508, section508.22.n | true | | landmark-banner-is-top-level | Ensures the banner landmark is at top level | Moderate | cat.semantics, best-practice | true | diff --git a/lib/checks/label/label-content-name-mismatch.js b/lib/checks/label/label-content-name-mismatch.js new file mode 100644 index 0000000000..bf79268a4e --- /dev/null +++ b/lib/checks/label/label-content-name-mismatch.js @@ -0,0 +1,49 @@ +const { text } = axe.commons; + +const accText = text.accessibleText(node).toLowerCase(); +if (text.isHumanInterpretable(accText) < 1) { + return undefined; +} + +const visibleText = text + .sanitize(text.visibleVirtual(virtualNode)) + .toLowerCase(); +if (text.isHumanInterpretable(visibleText) < 1) { + if (isStringContained(visibleText, accText)) { + return true; + } + return undefined; +} + +return isStringContained(visibleText, accText); + +/** + * Check if a given text exists in another + * + * @param {String} compare given text to check + * @param {String} compareWith text against which to be compared + * @returns {Boolean} + */ +function isStringContained(compare, compareWith) { + const curatedCompareWith = curateString(compareWith); + const curatedCompare = curateString(compare); + if (!curatedCompareWith || !curatedCompare) { + return false; + } + return curatedCompareWith.includes(curatedCompare); +} + +/** + * Curate given text, by removing emoji's, punctuations, unicode and trim whitespace. + * + * @param {String} str given text to curate + * @returns {String} + */ +function curateString(str) { + const noUnicodeStr = text.removeUnicode(str, { + emoji: true, + nonBmp: true, + punctuations: true + }); + return text.sanitize(noUnicodeStr); +} diff --git a/lib/checks/label/label-content-name-mismatch.json b/lib/checks/label/label-content-name-mismatch.json new file mode 100644 index 0000000000..818f1ce1a5 --- /dev/null +++ b/lib/checks/label/label-content-name-mismatch.json @@ -0,0 +1,11 @@ +{ + "id": "label-content-name-mismatch", + "evaluate": "label-content-name-mismatch.js", + "metadata": { + "impact": "serious", + "messages": { + "pass": "Element contains visible text as part of it's accessible name", + "fail": "Text inside the element is not included in the accessible name" + } + } +} diff --git a/lib/commons/aria/index.js b/lib/commons/aria/index.js index 0ca6fd0d23..3903806129 100644 --- a/lib/commons/aria/index.js +++ b/lib/commons/aria/index.js @@ -2357,3 +2357,45 @@ lookupTable.evaluateRoleForElement = { return out; } }; + +/** + * Reference -> https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#Widget_roles + * The current lookupTable.role['widget'] widget, yeilds + * -> + * [ + * "alert", "alertdialog", "button", "checkbox", "dialog", "gridcell", "link", "log", "marquee", "menuitem", "menuitemcheckbox", + * "menuitemradio", "option", "progressbar", "radio", "scrollbar", "searchbox", "slider", "spinbutton", "status", "switch", "tab", "tabpanel", + * "textbox", "timer", "tooltip", "treeitem" + * ] + * There are some differences against specs, hence the below listing was made + */ +lookupTable.rolesOfType = { + widget: [ + 'button', + 'checkbox', + 'dialog', + 'gridcell', + 'heading', + 'link', + 'log', + 'marquee', + 'menuitem', + 'menuitemcheckbox', + 'menuitemradio', + 'option', + 'progressbar', + 'radio', + 'scrollbar', + 'slider', + 'spinbutton', + 'status', + 'switch', + 'tab', + 'tabpanel', + 'textbox', + 'timer', + 'tooltip', + 'tree', + 'treeitem' + ] +}; diff --git a/lib/commons/text/is-human-interpretable.js b/lib/commons/text/is-human-interpretable.js new file mode 100644 index 0000000000..f93a434fdb --- /dev/null +++ b/lib/commons/text/is-human-interpretable.js @@ -0,0 +1,51 @@ +/* global text */ + +/** + * Determines if a given text is human friendly and interpretable + * + * @method isHumanInterpretable + * @memberof axe.commons.text + * @instance + * @param {String} str text to be validated + * @returns {Number} Between 0 and 1, (0 -> not interpretable, 1 -> interpretable) + */ +text.isHumanInterpretable = function(str) { + /** + * Steps: + * 1) Check for single character edge cases + * a) handle if character is alphanumeric & within the given icon mapping + * eg: x (close), i (info) + * + * 3) handle unicode from astral (non bilingual multi plane) unicode, emoji & punctuations + * eg: Windings font + * eg: 'πŸ’ͺ' + * eg: I saw a shooting πŸ’« + * eg: ? (help), > (next arrow), < (back arrow), need help ? + */ + + if (!str.length) { + return 0; + } + + // Step 1 + const alphaNumericIconMap = [ + 'x', // close + 'i' // info + ]; + // Step 1a + if (alphaNumericIconMap.includes(str)) { + return 0; + } + + // Step 2 + const noUnicodeStr = text.removeUnicode(str, { + emoji: true, + nonBmp: true, + punctuations: true + }); + if (!text.sanitize(noUnicodeStr)) { + return 0; + } + + return 1; +}; diff --git a/lib/commons/text/unicode.js b/lib/commons/text/unicode.js new file mode 100644 index 0000000000..24eb8214a6 --- /dev/null +++ b/lib/commons/text/unicode.js @@ -0,0 +1,117 @@ +/* global text */ + +/** + * Determine if a given string contains unicode characters, specified in options + * + * @method hasUnicode + * @memberof axe.commons.text + * @instance + * @param {String} str string to verify + * @param {Object} options config containing which unicode character sets to verify + * @property {Boolean} options.emoji verify emoji unicode + * @property {Boolean} options.nonBmp verify nonBmp unicode + * @property {Boolean} options.punctuations verify punctuations unicode + * @returns {Boolean} + */ +text.hasUnicode = function hasUnicode(str, options) { + const { emoji, nonBmp, punctuations } = options; + if (emoji) { + return axe.imports.emojiRegexText().test(str); + } + if (nonBmp) { + return getUnicodeNonBmpRegExp().test(str); + } + if (punctuations) { + return getPunctuationRegExp().test(str); + } + return false; +}; + +/** + * Remove specified type(s) unicode characters + * + * @method removeUnicode + * @memberof axe.commons.text + * @instance + * @param {String} str string to operate on + * @param {Object} options config containing which unicode character sets to remove + * @property {Boolean} options.emoji remove emoji unicode + * @property {Boolean} options.nonBmp remove nonBmp unicode + * @property {Boolean} options.punctuations remove punctuations unicode + * @returns {String} + */ +text.removeUnicode = function removeUnicode(str, options) { + const { emoji, nonBmp, punctuations } = options; + + if (emoji) { + str = str.replace(axe.imports.emojiRegexText(), ''); + } + if (nonBmp) { + str = str.replace(getUnicodeNonBmpRegExp(), ''); + } + if (punctuations) { + str = str.replace(getPunctuationRegExp(), ''); + } + + return str; +}; + +/** + * Regex for matching unicode values out of Basic Multilingual Plane (BMP) + * Reference: + * - https://github.com/mathiasbynens/regenerate + * - https://unicode-table.com/ + * - https://mathiasbynens.be/notes/javascript-unicode + * + * @returns {RegExp} + */ +function getUnicodeNonBmpRegExp() { + /** + * Regex for matching astral plane unicode + * - http://kourge.net/projects/regexp-unicode-block + */ + return new RegExp( + '[' + + '\u1D00-\u1D7F' + // Phonetic Extensions + '\u1D80-\u1DBF' + // Phonetic Extensions Supplement + '\u1DC0-\u1DFF' + // Combining Diacritical Marks Supplement + // '\u2000-\u206F' + // General punctuation - handled in -> getPunctuationRegExp + '\u20A0-\u20CF' + // Currency symbols + '\u20D0-\u20FF' + // Combining Diacritical Marks for Symbols + '\u2100-\u214F' + // Letter like symbols + '\u2150-\u218F' + // Number forms (eg: Roman numbers) + '\u2190-\u21FF' + // Arrows + '\u2200-\u22FF' + // Mathematical operators + '\u2300-\u23FF' + // Misc Technical + '\u2400-\u243F' + // Control pictures + '\u2440-\u245F' + // OCR + '\u2460-\u24FF' + // Enclosed alpha numerics + '\u2500-\u257F' + // Box Drawing + '\u2580-\u259F' + // Block Elements + '\u25A0-\u25FF' + // Geometric Shapes + '\u2600-\u26FF' + // Misc Symbols + '\u2700-\u27BF' + // Dingbats + ']' + ); +} + +/** + * Get regular expression for matching punctuations + * + * @returns {RegExp} + */ +function getPunctuationRegExp() { + /** + * Reference: http://kunststube.net/encoding/ + * US-ASCII + * -> !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ + * + * General Punctuation block + * -> \u2000-\u206F + * + * Supplemental Punctuation block + * Reference: https://en.wikipedia.org/wiki/Supplemental_Punctuation + * -> \u2E00-\u2E7F Reference + */ + return /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]/g; +} diff --git a/lib/core/imports/index.js b/lib/core/imports/index.js index 2693bc9392..ef799ca0e0 100644 --- a/lib/core/imports/index.js +++ b/lib/core/imports/index.js @@ -20,6 +20,7 @@ require('es6-promise').polyfill(); */ axe.imports = { axios: require('axios'), + CssSelectorParser: require('css-selector-parser').CssSelectorParser, doT: require('dot'), - CssSelectorParser: require('css-selector-parser').CssSelectorParser + emojiRegexText: require('emoji-regex') }; diff --git a/lib/rules/label-content-name-mismatch-matches.js b/lib/rules/label-content-name-mismatch-matches.js new file mode 100644 index 0000000000..61a605478c --- /dev/null +++ b/lib/rules/label-content-name-mismatch-matches.js @@ -0,0 +1,42 @@ +/** + * Applicability: + * Rule applies to any element that has + * a) a semantic role that is `widget` that supports name from content + * b) has visible text content + * c) has accessible name (eg: `aria-label`) + */ +const { aria, text } = axe.commons; + +const role = aria.getRole(node); +if (!role) { + return false; +} + +const isWidgetType = aria.lookupTable.rolesOfType.widget.includes(role); +if (!isWidgetType) { + return false; +} + +const rolesWithNameFromContents = aria.getRolesWithNameFromContents(); +if (!rolesWithNameFromContents.includes(role)) { + return false; +} + +/** + * if no `aria-label` or `aria-labelledby` attribute - ignore `node` + */ +if ( + !text.sanitize(aria.arialabelText(node)) && + !text.sanitize(aria.arialabelledbyText(node)) +) { + return false; +} + +/** + * if no `contentText` - ignore `node` + */ +if (!text.sanitize(text.visibleVirtual(virtualNode))) { + return false; +} + +return true; diff --git a/lib/rules/label-content-name-mismatch.json b/lib/rules/label-content-name-mismatch.json new file mode 100644 index 0000000000..cf08b6474a --- /dev/null +++ b/lib/rules/label-content-name-mismatch.json @@ -0,0 +1,12 @@ +{ + "id": "label-content-name-mismatch", + "matches": "label-content-name-mismatch-matches.js", + "tags": ["wcag21a", "wcag253", "experimental"], + "metadata": { + "description": "Ensures that elements labelled through their content must have their visible text as part of their accessible name", + "help": "Elements must have their visible text as part of their accessible name" + }, + "all": [], + "any": ["label-content-name-mismatch"], + "none": [] +} diff --git a/package.json b/package.json index 1f58f5d110..783a0d6fb4 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "clone": "~2.1.1", "css-selector-parser": "^1.3.0", "dot": "~1.1.2", + "emoji-regex": "7.0.3", "es6-promise": "^4.2.6", "eslint": "^5.14.0", "eslint-config-prettier": "^3.4.0", diff --git a/test/checks/label/label-content-name-mismatch.js b/test/checks/label/label-content-name-mismatch.js new file mode 100644 index 0000000000..e48f3f0f82 --- /dev/null +++ b/test/checks/label/label-content-name-mismatch.js @@ -0,0 +1,150 @@ +describe('label-content-name-mismatch tests', function() { + 'use strict'; + + var fixture = document.getElementById('fixture'); + var queryFixture = axe.testUtils.queryFixture; + var check = checks['label-content-name-mismatch']; + var options = undefined; + + afterEach(function() { + fixture.innerHTML = ''; + }); + + it('returns true when visible text and accessible name (`aria-label`) matches (text sanitized)', function() { + var vNode = queryFixture( + '
next page
' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isTrue(actual); + }); + + it('returns true when visible text and accessible name (`aria-label`) matches (character insensitive)', function() { + var vNode = queryFixture( + '
next pAge
' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isTrue(actual); + }); + + it('returns true when visible text and accessible name (`aria-labelledby`) matches (character insensitive & text sanitized)', function() { + var vNode = queryFixture( + '
UNTIL THE VeRy EnD
' + + '
uNtIl the very end  
' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isTrue(actual); + }); + + it('returns true when visible text is contained in the accessible name', function() { + var vNode = queryFixture( + '' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isTrue(actual); + }); + + it('returns false when visible text doesn’t match accessible name', function() { + var vNode = queryFixture( + '
Next
' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isFalse(actual); + }); + + it('returns false when not all of visible text is included in accessible name', function() { + var vNode = queryFixture( + '' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isFalse(actual); + }); + + it('returns false when element has non-matching accessible name (`aria-labelledby`) and text content', function() { + var vNode = queryFixture( + '
some content
' + + '
123
' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isFalse(actual); + }); + + it('returns true when visible text excluding emoji is part of accessible name', function() { + var vNode = queryFixture( + '' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isTrue(actual); + }); + + it('returns true when visible text excluding punctuations/ symbols is part of accessible name', function() { + var vNode = queryFixture( + '' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isTrue(actual); + }); + + it('returns undefined (needs review) when visible text name is only an emoji', function() { + var vNode = queryFixture( + '' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isUndefined(actual); + }); + + it('returns undefined (needs review) when accessible name is an emoji', function() { + var vNode = queryFixture( + '' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isUndefined(actual); + }); + + it('returns undefined (needs review) for visible text is single characters (punctuation) used as icon', function() { + var vNode = queryFixture( + '' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isUndefined(actual); + }); + + it('returns undefined (needs review) for unicode as accessible name and text content', function() { + var vNode = queryFixture( + '' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isUndefined(actual); + }); + + it('returns undefined (needs review) for unicode text content', function() { + var vNode = queryFixture( + '' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isUndefined(actual); + }); + + it('returns undefined (needs review) when punctuation is used as text content', function() { + var vNode = queryFixture( + '' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isUndefined(actual); + }); + + it('returns true when normal text content which is punctuated', function() { + var vNode = queryFixture( + '' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isTrue(actual); + }); + + it('returns false when normal puntuated text content is not contained in accessible name is punctuated', function() { + var vNode = queryFixture( + '' + ); + var actual = check.evaluate(vNode.actualNode, options, vNode); + assert.isFalse(actual); + }); +}); diff --git a/test/commons/text/accessible-text.js b/test/commons/text/accessible-text.js index 95a3570f3a..9280a82b6f 100644 --- a/test/commons/text/accessible-text.js +++ b/test/commons/text/accessible-text.js @@ -2902,7 +2902,6 @@ describe('text.accessibleTextVirtual', function() { axe._tree = axe.utils.getFlattenedTree(fixture); var target = fixture.querySelector('#test'); - // Chrome 72: "My name is Eli the weird. (QED) Where are my marbles?" // Safari 12.0: "My name is Eli the weird. (QED) Where are my marbles?" // Firefox 62: "Hello, My name is Eli the weird. (QED)" diff --git a/test/commons/text/is-human-interpretable.js b/test/commons/text/is-human-interpretable.js new file mode 100644 index 0000000000..93716f36f8 --- /dev/null +++ b/test/commons/text/is-human-interpretable.js @@ -0,0 +1,63 @@ +describe('text.isHumanInterpretable', function() { + it('returns 0 when given string is empty', function() { + var actual = axe.commons.text.isHumanInterpretable(''); + assert.equal(actual, 0); + }); + + it('returns 0 when given string is a single character that is blacklisted as icon', function() { + var blacklistedIcons = ['x', 'i']; + blacklistedIcons.forEach(function(iconText) { + var actual = axe.commons.text.isHumanInterpretable(iconText); + assert.equal(actual, 0); + }); + }); + + it('returns 0 when given string is only punctuations', function() { + var actual = axe.commons.text.isHumanInterpretable('?!!!,.'); + assert.equal(actual, 0); + }); + + it('returns 1 when given string has emoji as a part of the sentence', function() { + var actual = axe.commons.text.isHumanInterpretable('I like πŸ€'); + assert.equal(actual, 1); + }); + + it('returns 1 when given string has non BMP character (eg: windings font) as part of the sentence', function() { + var actual = axe.commons.text.isHumanInterpretable('I βœ‚ my hair'); + assert.equal(actual, 1); + }); + + it('returns 1 when given string has both non BMP character, and emoji as part of the sentence', function() { + var actual = axe.commons.text.isHumanInterpretable( + 'I βœ‚ my hair, and I like πŸ€' + ); + assert.equal(actual, 1); + }); + + it('returns 0 when given string has only emoji', function() { + var actual = axe.commons.text.isHumanInterpretable('πŸ€πŸ”πŸ‰πŸŽ…'); + assert.equal(actual, 0); + }); + + it('returns 0 when given string has only non BNP characters', function() { + var actual = axe.commons.text.isHumanInterpretable('βŒ›πŸ‘“'); + assert.equal(actual, 0); + }); + + it('returns 0 when given string has combination of only non BNP characters and emojis', function() { + var actual = axe.commons.text.isHumanInterpretable('βŒ›πŸ‘“πŸ€πŸ”πŸ‰πŸŽ…'); + assert.equal(actual, 0); + }); + + it('returns 1 when given string is a punctuated sentence', function() { + var actual = axe.commons.text.isHumanInterpretable( + "I like football, but I prefer basketball; although I can't play either very well." + ); + assert.equal(actual, 1); + }); + + it('returns 1 for a sentence without emoji or punctuations', function() { + var actual = axe.commons.text.isHumanInterpretable('Earth is round'); + assert.equal(actual, 1); + }); +}); diff --git a/test/commons/text/unicode.js b/test/commons/text/unicode.js new file mode 100644 index 0000000000..cf0429830b --- /dev/null +++ b/test/commons/text/unicode.js @@ -0,0 +1,201 @@ +describe('text.hasUnicode', function() { + describe('text.hasUnicode, characters of type Non Bi Multilingual Plane', function() { + it('returns false when given string is alphanumeric', function() { + var actual = axe.commons.text.hasUnicode('1 apple', { + nonBmp: true + }); + assert.isFalse(actual); + }); + + it('returns false when given string is number', function() { + var actual = axe.commons.text.hasUnicode('100', { + nonBmp: true + }); + assert.isFalse(actual); + }); + + it('returns false when given string is a sentence', function() { + var actual = axe.commons.text.hasUnicode('Earth is round', { + nonBmp: true + }); + assert.isFalse(actual); + }); + + it('returns true when given string is a phonetic extension', function() { + var actual = axe.commons.text.hasUnicode('ᴁ', { + nonBmp: true + }); + assert.isTrue(actual); + }); + + it('returns true when given string is a combining diacritical marks supplement', function() { + var actual = axe.commons.text.hasUnicode('ᴁ', { + nonBmp: true + }); + assert.isTrue(actual); + }); + + it('returns true when given string is a currency symbols', function() { + var actual = axe.commons.text.hasUnicode('₨ 20000', { + nonBmp: true + }); + assert.isTrue(actual); + }); + + it('returns true when given string has arrows', function() { + var actual = axe.commons.text.hasUnicode('← turn left', { + nonBmp: true + }); + assert.isTrue(actual); + }); + + it('returns true when given string has geometric shapes', function() { + var actual = axe.commons.text.hasUnicode('β—“', { + nonBmp: true + }); + assert.isTrue(actual); + }); + + it('returns true when given string has math operators', function() { + var actual = axe.commons.text.hasUnicode('√4 = 2', { + nonBmp: true + }); + assert.isTrue(actual); + }); + + it('returns true when given string has windings font', function() { + var actual = axe.commons.text.hasUnicode('β–½', { + nonBmp: true + }); + assert.isTrue(actual); + }); + }); + + describe('text.hasUnicode, characters of type Emoji', function() { + it('returns false when given string is alphanumeric', function() { + var actual = axe.commons.text.hasUnicode( + '1 apple a day, keeps the doctor away', + { + emoji: true + } + ); + assert.isFalse(actual); + }); + + it('returns false when given string is number', function() { + var actual = axe.commons.text.hasUnicode('100', { + emoji: true + }); + assert.isFalse(actual); + }); + + it('returns false when given string is a sentence', function() { + var actual = axe.commons.text.hasUnicode('Earth is round', { + emoji: true + }); + assert.isFalse(actual); + }); + + it('returns true when given string has emoji', function() { + var actual = axe.commons.text.hasUnicode('🌎 is round', { + emoji: true + }); + assert.isTrue(actual); + }); + + it('returns true when given string has emoji', function() { + var actual = axe.commons.text.hasUnicode('plant a 🌱', { + emoji: true + }); + assert.isTrue(actual); + }); + }); + + describe('text.hasUnicode, characters of type punctuations', function() { + it('returns false when given string is number', function() { + var actual = axe.commons.text.hasUnicode('100', { + punctuations: true + }); + assert.isFalse(actual); + }); + + it('returns false when given string is a sentence', function() { + var actual = axe.commons.text.hasUnicode('Earth is round', { + punctuations: true + }); + assert.isFalse(actual); + }); + + it('returns true when given string has punctuations', function() { + var actual = axe.commons.text.hasUnicode("What's your name?", { + punctuations: true + }); + assert.isTrue(actual); + }); + }); + + describe('text.hasUnicode, has combination of unicode', function() { + it('returns false when given string is number', function() { + var actual = axe.commons.text.hasUnicode('100', { + emoji: true, + nonBmp: true, + punctuations: true + }); + assert.isFalse(actual); + }); + + it('returns true when given string has unicode characters', function() { + var actual = axe.commons.text.hasUnicode( + 'The β˜€οΈ is orange, the β—“ is white.', + { + emoji: true, + nonBmp: true, + punctuations: true + } + ); + assert.isTrue(actual); + }); + }); +}); + +describe('text.removeUnicode', function() { + it('returns string by removing non BMP unicode ', function() { + var actual = axe.commons.text.removeUnicode('₨20000', { + nonBmp: true + }); + assert.equal(actual, '20000'); + }); + + it('returns string by removing emoji unicode ', function() { + var actual = axe.commons.text.removeUnicode('β˜€οΈSun 🌎Earth', { + emoji: true + }); + assert.equal(actual, 'Sun Earth'); + }); + + it('returns string after removing punctuations from word', function() { + var actual = axe.commons.text.removeUnicode('Earth!!!', { + punctuations: true + }); + assert.equal(actual, 'Earth'); + }); + + it('returns string removing all punctuations', function() { + var actual = axe.commons.text.removeUnicode('', { + punctuations: true + }); + assert.equal(actual, ''); + }); + + it('returns string removing combination of unicode characters', function() { + var actual = axe.commons.text.removeUnicode( + 'The β˜€οΈ is orange, the β—“ is white.', + { + emoji: true, + nonBmp: true, + punctuations: true + } + ); + assert.equal(actual, 'The is orange the is white'); + }); +}); diff --git a/test/integration/rules/label-content-name-mismatch/label-content-name-mismatch.html b/test/integration/rules/label-content-name-mismatch/label-content-name-mismatch.html new file mode 100644 index 0000000000..ac434b5ed0 --- /dev/null +++ b/test/integration/rules/label-content-name-mismatch/label-content-name-mismatch.html @@ -0,0 +1,35 @@ + +
next page
+
next page
+ +
next page
+
next pAge
+
uNtIl the very end  
+
UNTIL THE VeRy EnD
+ + + +
Next
+ +
Next
+ +
123
+
some content
+ + + + + + + + + + + + + + + +Next + + diff --git a/test/integration/rules/label-content-name-mismatch/label-content-name-mismatch.json b/test/integration/rules/label-content-name-mismatch/label-content-name-mismatch.json new file mode 100644 index 0000000000..ccd7506439 --- /dev/null +++ b/test/integration/rules/label-content-name-mismatch/label-content-name-mismatch.json @@ -0,0 +1,27 @@ +{ + "description": "label-content-name-mismatch tests", + "rule": "label-content-name-mismatch", + "violations": [["#fail1"], ["#fail2"], ["#fail3"], ["#fail4"], ["#fail5"]], + "passes": [ + ["#pass1"], + ["#pass2"], + ["#pass3"], + ["#pass4"], + ["#pass5"], + ["#pass6"], + ["#pass7"] + ], + "incomplete": [ + ["#incomplete1"], + ["#incomplete2"], + ["#incomplete3"], + ["#incomplete4"], + ["#incomplete5"], + ["#incomplete6"], + ["#incomplete7"], + ["#incomplete8"], + ["#incomplete9"], + ["#incomplete10"], + ["#incomplete11"] + ] +} diff --git a/test/rule-matches/label-content-name-mismatch-matches.js b/test/rule-matches/label-content-name-mismatch-matches.js new file mode 100644 index 0000000000..c0e884447f --- /dev/null +++ b/test/rule-matches/label-content-name-mismatch-matches.js @@ -0,0 +1,157 @@ +describe('label-content-name-mismatch-matches tests', function() { + 'use strict'; + + var fixture = document.getElementById('fixture'); + var queryFixture = axe.testUtils.queryFixture; + var rule = axe._audit.rules.find(function(rule) { + return rule.id === 'label-content-name-mismatch'; + }); + + afterEach(function() { + fixture.innerHTML = ''; + axe._tree = undefined; + }); + + it('returns false if given element has no role', function() { + var vNode = queryFixture( + '
' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isFalse(actual); + }); + + it('returns false if element role is not supported with name from contents', function() { + var vNode = queryFixture( + '
20 %
' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isFalse(actual); + }); + + it('returns false if implicit element role is overridden to a role that does not support name from contents', function() { + var vNode = queryFixture( + '
Status message
' + + '' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isFalse(actual); + }); + + it('returns false if element does not have accessible name attributes (`aria-label` or `aria-labelledby`)', function() { + var vNode = queryFixture( + '' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isFalse(actual); + }); + + it('returns false if element has empty accessible name via `aria-label`', function() { + var vNode = queryFixture( + '' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isFalse(actual); + }); + + it('returns true if element has accessible name via `aria-label`', function() { + var vNode = queryFixture( + '' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isTrue(actual); + }); + + it('returns true if element has accessible name via `aria-labelledby`', function() { + var vNode = queryFixture( + '
some content
' + + '
Foo text
' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isTrue(actual); + }); + + it('returns false if element has empty accessible name (`aria-labelledby`)', function() { + var vNode = queryFixture( + '
some content
' + + '
' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isFalse(actual); + }); + + it('returns false if element has empty accessible name (`aria-labelledby`) because idref does not exist', function() { + var vNode = queryFixture( + '
some content
' + + '
Right Label
' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isFalse(actual); + }); + + it('returns true if element has accessible name (`aria-labelledby`) - multiple refs', function() { + var vNode = queryFixture( + '
some content
' + + '
Foo
' + + '
Bar
' + + '
Baz
' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isTrue(actual); + }); + + it('returns false for non-widget role', function() { + var vNode = queryFixture( + 'Content Information' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isFalse(actual); + }); + + it('returns true for widget role that does support name from content', function() { + var vNode = queryFixture( + '' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isTrue(actual); + }); + + it('returns false for empty text content', function() { + var vNode = queryFixture( + '' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isFalse(actual); + }); + + it('returns false for non text content', function() { + var vNode = queryFixture( + '' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isFalse(actual); + }); + + it('returns false for hidden (non visible) text content', function() { + var vNode = queryFixture( + '' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isFalse(actual); + }); + + it('returns true when visible text is combination of alphanumeric and emoji characters', function() { + var vNode = queryFixture( + '' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isTrue(actual); + }); + + it('returns true when visible text is combination of alphanumeric and punctuation characters', function() { + var vNode = queryFixture( + '' + ); + var actual = rule.matches(vNode.actualNode, vNode); + assert.isTrue(actual); + }); +}); diff --git a/test/testutils.js b/test/testutils.js index ba06e16021..f848588556 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -71,7 +71,7 @@ testUtils.shadowSupport = (function(document) { * Method for injecting content into a fixture and caching * the flattened DOM tree (light and Shadow DOM together) * - * @param Node|String Stuff to go into the fixture (html or DOM node) + * @param {String|Node} content Stuff to go into the fixture (html or DOM node) * @return HTMLElement */ testUtils.fixtureSetup = function(content) { @@ -275,10 +275,15 @@ testUtils.addStyleSheets = function addStyleSheets(sheets) { return q; }; -testUtils.queryFixture = function queryFixture (html, query) { - testUtils.fixtureSetup(html) - return axe.utils.querySelectorAll(axe._tree, (query || '#target'))[0]; -} - +/** + * Injecting content into a fixture and return queried element within fixture + * + * @param {String|Node} content to go into the fixture (html or DOM node) + * @return HTMLElement + */ +testUtils.queryFixture = function queryFixture(html, query) { + testUtils.fixtureSetup(html); + return axe.utils.querySelectorAll(axe._tree, query || '#target')[0]; +}; axe.testUtils = testUtils;