Skip to content

Commit

Permalink
feat: rule for label and name from content mismatch
Browse files Browse the repository at this point in the history
  • Loading branch information
jeeyyy committed Jan 28, 2019
1 parent c3a1bdc commit ef1c638
Show file tree
Hide file tree
Showing 10 changed files with 431 additions and 7 deletions.
1 change: 1 addition & 0 deletions doc/rule-descriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 labeled 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 |
Expand Down
25 changes: 25 additions & 0 deletions lib/checks/label/label-content-name-mismatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const {
text: {
accessibleText,
visible // get visible text
}
} = axe.commons;

/**
* Note:
* `label-content-name-mismatch-matches` ignore `node`
* if there is no `accessibleText` or `textContent`
*/
const accText = accessibleText(node).toLowerCase();
const visibleTextContent = visible(node).toLowerCase();

/**
* if `textContent` is not part of `accessibleText` -> fail
*/

if (!accText.includes(visibleTextContent)) {
return false;
}

// -> pass
return true;
11 changes: 11 additions & 0 deletions lib/checks/label/label-content-name-mismatch.json
Original file line number Diff line number Diff line change
@@ -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": "The visible text of the element is different from it's accessible name"
}
}
}
62 changes: 62 additions & 0 deletions lib/rules/label-content-name-mismatch-matches.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* 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 an accessible name (eg: `aria-label`)
*/
const {
aria: {
arialabelText,
arialabelledbyText,
getRole,
getRolesWithNameFromContents
},
text: {
accessibleText,
visible: visibleText // -> get visible text
}
} = axe.commons;

const role = getRole(node);
/**
* if no `role` - ignore `node`
*/
if (!role) {
return false;
}

const rolesWithNameFromContents = getRolesWithNameFromContents();
/**
* if no `rolesWithNameFromContents` - ignore `node`
*/
if (!rolesWithNameFromContents) {
return false;
}

/**
* if no `aria-label` or `aria-labelledby` attribute - ignore `node`
*/
const isAriaLabel = arialabelText(node);
const isAriaLabelledBy = arialabelledbyText(node);
if (!isAriaLabel && !isAriaLabelledBy) {
return false;
}

const visibleTextContent = visibleText(node);
/**
* if no `contentText` or contains `unicode` - ignore `node`
*/
if (!visibleTextContent || /[^\u0000-\u00ff]/.test(visibleTextContent)) {
return false;
}

const accText = accessibleText(node);
/**
* if no `accessibleText` or contains `unicode` - ignore `node`
*/
if (!accText || /[^\u0000-\u00ff]/.test(accText)) {
return false;
}

return true;
12 changes: 12 additions & 0 deletions lib/rules/label-content-name-mismatch.json
Original file line number Diff line number Diff line change
@@ -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 labeled 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": []
}
105 changes: 105 additions & 0 deletions test/checks/label/label-content-name-mismatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
describe('label-content-name-mismatch tests', function() {
'use strict';

var fixture = document.getElementById('fixture');
var fixtureSetup = axe.testUtils.fixtureSetup;
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 target = fixtureSetup(
'<div id="target" role="link" aria-label="next page %20 ">next page</div>',
'div#target'
);
var actual = check.evaluate(
target,
options,
axe.utils.getNodeFromTree(axe._tree[0], target)
);
assert.isTrue(actual);
});

it('returns true when visible text and accessible name (`aria-label`) matches (character insensitive)', function() {
var target = fixtureSetup(
'<div id="target" role="link" aria-label="Next Page">next pAge</div>',
'div#target'
);
var actual = check.evaluate(
target,
options,
axe.utils.getNodeFromTree(axe._tree[0], target)
);
assert.isTrue(actual);
});

it('returns true when visible text and accessible name (`aria-labelledby`) matches (character insensitive & text sanitized)', function() {
var target = fixtureSetup(
'<div id="target" aria-labelledby="yourLabel">UNTIL THE VeRy EnD</div>' +
'<div id="yourLabel">uNtIl the very end %20</div>',
'div#target'
);
var actual = check.evaluate(
target,
options,
axe.utils.getNodeFromTree(axe._tree[0], target)
);
assert.isTrue(actual);
});

it('returns true when visible text is contained in the accessible name', function() {
var target = fixtureSetup(
'<button id="target" name="link" aria-label="Next Page in the list">Next Page</button>',
'#target'
);
var actual = check.evaluate(
target,
options,
axe.utils.getNodeFromTree(axe._tree[0], target)
);
assert.isTrue(actual);
});

it('returns false when visible text doesn’t match accessible name', function() {
var target = fixtureSetup(
'<div id="target" role="link" aria-label="OK">Next</div>',
'#target'
);
var actual = check.evaluate(
target,
options,
axe.utils.getNodeFromTree(axe._tree[0], target)
);
assert.isFalse(actual);
});

it('returns false when not all of visible text is included in accessible name', function() {
var target = fixtureSetup(
'<button id="target" name="link" aria-label="the full">The full label</button>',
'#target'
);
var actual = check.evaluate(
target,
options,
axe.utils.getNodeFromTree(axe._tree[0], target)
);
assert.isFalse(actual);
});

it('returns false when element has non-matching accessible name (`aria-labelledby`) and text content', function() {
var target = fixtureSetup(
'<div role="button" id="target" aria-labelledby="foo">some content</div>' +
'<div id="foo">123</div>',
'div#target'
);
var actual = check.evaluate(
target,
options,
axe.utils.getNodeFromTree(axe._tree[0], target)
);
assert.isFalse(actual);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- Pass -->
<div id='pass1' role="link" aria-label="next page ">next page</div>
<div id='pass2' role="link" aria-label="Next Page">next page</div>
<button id='pass3' name="link" aria-label="Next Page in the list">Next Page</button>
<div id="pass4" role="link" aria-label="next page %20 ">next page</div>
<div id="pass5" role="link" aria-label="Next Page">next pAge</div>

<div role='button' id="pass6" aria-labelledby="yourLabel">UNTIL THE VeRy EnD</div>
<div id="yourLabel">uNtIl the very end %20</div>

<button id="pass7" name="link" aria-label="Next Page in the list">Next Page</button>

<!-- Fail -->
<div id="fail1" role="link" aria-label="OK">Next</div>
<button id="fail2" name="link" aria-label="the full">The full label</button>
<div id="fail3" role="link" aria-label="OK">Next</div>
<button id="fail4" name="link" aria-label="the full">The full label</button>

<div role="button" id="fail5" aria-labelledby="foo">some content</div>
<div id="foo">123</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"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"]
]
}
Loading

0 comments on commit ef1c638

Please sign in to comment.