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(
+ '
Next
'
+ );
+ 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;