Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 36 additions & 12 deletions lib/checks/label/label-content-name-mismatch-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,58 @@ import {
} from '../../commons/text';

/**
* Check if a given text exists in another
* Check whether the words of `compare` appear as a contiguous run of words
* within `compareWith`, following ACT rule 2ee8b8's "label in name" algorithm:
* non-letter/non-digit characters are treated as word separators and the
* comparison is done on whole words, not raw substrings.
Comment thread
chutchins25 marked this conversation as resolved.
Outdated
*
* @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) {
const compareTokens = curateTokens(compare);
const compareWithTokens = curateTokens(compareWith);
if (!compareTokens.length || !compareWithTokens.length) {
return false;
}
return curatedCompareWith.includes(curatedCompare);
return isContiguousSubsequence(compareWithTokens, compareTokens);
Comment thread
chutchins25 marked this conversation as resolved.
Outdated
}

/**
* Curate given text, by removing emoji's, punctuations, unicode and trim whitespace.
* Tokenize text the way ACT rule 2ee8b8's "label in name" algorithm does:
* treat non-text characters (emoji, punctuation, symbols) as word separators
* by replacing them with a space, then split into words. Uses `removeUnicode`'s
* explicit unicode ranges (rather than a `\p{…}` property escape) to keep the
* comparison working on the browsers axe supports.
*
* @param {String} str given text to curate
* @returns {String}
* @param {String} str given text to tokenize
* @returns {String[]}
*/
function curateString(str) {
const noUnicodeStr = removeUnicode(str, {
function curateTokens(str) {
const separated = removeUnicode(str, {
emoji: true,
nonBmp: true,
punctuations: true
punctuations: true,
replaceWith: ' '
});
return sanitize(noUnicodeStr);
return sanitize(separated).split(' ').filter(Boolean);
Comment thread
chutchins25 marked this conversation as resolved.
Outdated
}

/**
* Whether `sub` appears as a contiguous run within `sequence`.
*
* @param {String[]} sequence
* @param {String[]} sub
* @returns {Boolean}
*/
function isContiguousSubsequence(sequence, sub) {
for (let i = 0; i + sub.length <= sequence.length; i++) {
if (sub.every((word, j) => word === sequence[i + j])) {
return true;
}
}
return false;
}

function labelContentNameMismatchEvaluate(node, options, virtualNode) {
Expand Down
13 changes: 7 additions & 6 deletions lib/commons/text/remove-unicode.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,23 @@ import { emojiRegexText } from '../../core/imports';
* @property {Boolean} options.emoji remove emoji unicode
* @property {Boolean} options.nonBmp remove nonBmp unicode
* @property {Boolean} options.punctuations remove punctuations unicode
* @property {String} [options.replaceWith=''] string to substitute for each matched character (e.g. a space to preserve word boundaries)
* @returns {String}
*/
function removeUnicode(str, options) {
const { emoji, nonBmp, punctuations } = options;
const { emoji, nonBmp, punctuations, replaceWith = '' } = options;

if (emoji) {
str = str.replace(emojiRegexText(), '');
str = str.replace(emojiRegexText(), replaceWith);
}
if (nonBmp) {
str = str
.replace(getUnicodeNonBmpRegExp(), '')
.replace(getSupplementaryPrivateUseRegExp(), '')
.replace(getCategoryFormatRegExp(), '');
.replace(getUnicodeNonBmpRegExp(), replaceWith)
.replace(getSupplementaryPrivateUseRegExp(), replaceWith)
.replace(getCategoryFormatRegExp(), replaceWith);
}
if (punctuations) {
str = str.replace(getPunctuationRegExp(), '');
str = str.replace(getPunctuationRegExp(), replaceWith);
Comment thread
chutchins25 marked this conversation as resolved.
Outdated
}

return str;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,5 @@ require('./act-runner.js')({
id: '2ee8b8',
title: 'Visible label is part of accessible name',
axeRules: ['label-content-name-mismatch'],
skipTests: [
// See: https://github.com/dequelabs/axe-core/issues/4311
'e9bbdbec137223e2973c6d2896050770c84c26e5',
// See: https://github.com/dequelabs/axe-core/issues/5207
'fab659b02c1edb4f2c8f0bda524b1076abab7df6',
'94a7ce7aea9dbfaa375c459c26d3a5923de84e7a',
'e117393d6711d6bdf32821005219c9d9474dfeb8',
'f5c9811c984987443476760a1c5b91b1067f7e19'
]
skipTests: []
Comment thread
chutchins25 marked this conversation as resolved.
Outdated
});
32 changes: 32 additions & 0 deletions test/checks/label/label-content-name-mismatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,36 @@ describe('label-content-name-mismatch tests', () => {
assert.isFalse(actual);
}
);

it('returns false when a hyphen joins words that the accessible name keeps separate', () => {
const vNode = queryFixture(
'<a id="target" href="#" aria-label="non-standard">nonstandard</a>'
);
const actual = check.evaluate(vNode.actualNode, options, vNode);
assert.isFalse(actual);
});

it('returns false when visible text is a single word not present as a whole word in the accessible name', () => {
const vNode = queryFixture(
'<a id="target" href="#" aria-label="email">e-mail</a>'
);
const actual = check.evaluate(vNode.actualNode, options, vNode);
assert.isFalse(actual);
});

it('returns false when the visible words are not a contiguous run within the accessible name', () => {
const vNode = queryFixture(
'<button id="target" aria-label="the big red button">big button</button>'
);
const actual = check.evaluate(vNode.actualNode, options, vNode);
assert.isFalse(actual);
});

it('returns true when the visible words are a contiguous run within the accessible name', () => {
const vNode = queryFixture(
'<button id="target" aria-label="go to next page now">next page</button>'
);
const actual = check.evaluate(vNode.actualNode, options, vNode);
assert.isTrue(actual);
});
});
8 changes: 8 additions & 0 deletions test/commons/text/unicode.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,12 @@ describe('text.removeUnicode', () => {
});
assert.equal(actual, 'Hello World');
});

it('substitutes matched characters with replaceWith when provided', () => {
Comment thread
chutchins25 marked this conversation as resolved.
Outdated
const actual = axe.commons.text.removeUnicode('non-standard', {
punctuations: true,
replaceWith: ' '
});
assert.equal(actual, 'non standard');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
</svg>
Hello Deque Systems
</a>
<a id="fail8" href="#" aria-label="non-standard">nonstandard</a>
Comment thread
chutchins25 marked this conversation as resolved.
Outdated

<!-- incomplete -->
<button id="incomplete1" aria-label="comet">☄️</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
["#fail4"],
["#fail5"],
["#fail6"],
["#fail7"]
["#fail7"],
["#fail8"]
],
"passes": [
["#pass1"],
Expand Down
Loading