Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rule): xml-lang-mismatch #999

Merged
merged 14 commits into from
Aug 16, 2018
Merged
Show file tree
Hide file tree
Changes from 13 commits
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
1 change: 1 addition & 0 deletions doc/rule-descriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
| hidden-content | Informs users about hidden content. | Minor | cat.structure, experimental, review-item | true |
| html-has-lang | Ensures every HTML document has a lang attribute | Serious | cat.language, wcag2a, wcag311 | true |
| html-lang-valid | Ensures the lang attribute of the <html> element has a valid value | Serious | cat.language, wcag2a, wcag311 | true |
| html-xml-lang-mismatch | Ensure that HTML elements with both valid lang and xml:lang attributes agree on the base language of the page | Moderate | cat.language, wcag2a, wcag311 | true |
| 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 |
Expand Down
13 changes: 4 additions & 9 deletions lib/checks/language/valid-lang.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
function getBaseLang(lang) {
return lang
.trim()
.split('-')[0]
.toLowerCase();
}

var langs, invalid;

langs = (options ? options : axe.commons.utils.validLangs()).map(getBaseLang);
langs = (options ? options : axe.commons.utils.validLangs()).map(
axe.commons.utils.getBaseLang
);

invalid = ['lang', 'xml:lang'].reduce(function(invalid, langAttr) {
var langVal = node.getAttribute(langAttr);
if (typeof langVal !== 'string') {
return invalid;
}

var baselangVal = getBaseLang(langVal);
var baselangVal = axe.commons.utils.getBaseLang(langVal);

// Edge sets lang to an empty string when xml:lang is set
// so we need to ignore empty strings here
Expand Down
5 changes: 5 additions & 0 deletions lib/checks/language/xml-lang-mismatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const { getBaseLang } = axe.commons.utils;
const primaryLangValue = getBaseLang(node.getAttribute('lang'));
const primaryXmlLangValue = getBaseLang(node.getAttribute('xml:lang'));

return primaryLangValue === primaryXmlLangValue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be the cleanest looking check we've got. Great job Jey.

11 changes: 11 additions & 0 deletions lib/checks/language/xml-lang-mismatch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "xml-lang-mismatch",

This comment was marked as outdated.

This comment was marked as resolved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorted!

"evaluate": "xml-lang-mismatch.js",
"metadata": {
"impact": "moderate",
"messages": {
"pass": "Lang and xml:lang attributes have the same base language",
"fail": "Lang and xml:lang attributes do not have the same base language"
}
}
}
18 changes: 18 additions & 0 deletions lib/commons/utils/get-base-lang.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* global axe */
/**
* Convenience function to extract primary language subtag from a given value
* @method getBaseLang
* @memberof axe.commons.utils
* @instance
* @param {String} value value specified as lang or xml:lang attribute
* @return {String}
*/
axe.utils.getBaseLang = function getBaseLang(lang) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very minor: I'd suggest letting this function handle the null case, so that you don't have to do it separately a bunch of times.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorted!

if (!lang) {
return '';
}
return lang
.trim()
.split('-')[0]
.toLowerCase();
};
19 changes: 19 additions & 0 deletions lib/rules/html-xml-lang-mismatch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"id": "html-xml-lang-mismatch",
"selector": "html[lang][xml\\:lang]",
"matches": "xml-lang-mismatch-matches.js",
"tags": [
"cat.language",
"wcag2a",
"wcag311"
],
"metadata": {
"description": "Ensure that HTML elements with both valid lang and xml:lang attributes agree on the base language of the page",
"help": "HTML elements with lang and xml:lang must have the same base language"
},
"all": [
"xml-lang-mismatch"
],
"any": [],
"none": []
}
12 changes: 12 additions & 0 deletions lib/rules/xml-lang-mismatch-matches.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// using -> "selector": "html[lang][xml\\:lang]" to narrow down html with lang and xml:lang attributes

// get primary base language for each of the attributes
const { getBaseLang } = axe.commons.utils;
const primaryLangValue = getBaseLang(node.getAttribute('lang'));
const primaryXmlLangValue = getBaseLang(node.getAttribute('xml:lang'));

// ensure that the value specified is valid lang for both `lang` and `xml:lang`
return (
axe.utils.validLangs().includes(primaryLangValue) &&
axe.utils.validLangs().includes(primaryXmlLangValue)
);
71 changes: 71 additions & 0 deletions test/checks/language/xml-lang-mismatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
describe('xml-lang-mismatch', function() {
'use strict';

var node;
var fixture = document.getElementById('fixture');
var checkContext = axe.testUtils.MockCheckContext();

beforeEach(function() {
// using a div element (instead of html), as the check is agnostic of element type
node = document.createElement('div');
});

afterEach(function() {
fixture.innerHTML = '';
checkContext.reset();
});

// the rule matches filters out node of type HTML, and tests cover this scenario to ensure other elements are not allowed for this check
// hence below tests are only for HTML element, although the logic in the check looks for matches in value os lang and xml:lang
// rather than node type match - hence the check can be re-used.

it('should return false if a only lang is supplied', function() {
node.setAttribute('lang', 'en');
fixture.appendChild(node);
assert.isFalse(
checks['xml-lang-mismatch'].evaluate.call(checkContext, node)
);
});

it('should return false if a only xml:lang is supplied albeit with region', function() {
node.setAttribute('xml:lang', 'fr-FR');
fixture.appendChild(node);
assert.isFalse(
checks['xml-lang-mismatch'].evaluate.call(checkContext, node)
);
});

it('should return false if lang is undefined', function() {
node.setAttribute('lang', undefined);
fixture.appendChild(node);
assert.isFalse(
checks['xml-lang-mismatch'].evaluate.call(checkContext, node)
);
});

it('should return true if lang and xml:lang is identical', function() {
node.setAttribute('lang', 'en-GB');
node.setAttribute('xml:lang', 'en-GB');
fixture.appendChild(node);
assert.isTrue(
checks['xml-lang-mismatch'].evaluate.call(checkContext, node)
);
});

it('should return true if lang and xml:lang have identical primary sub tag', function() {
node.setAttribute('lang', 'en-GB');
node.setAttribute('xml:lang', 'en-US');
fixture.appendChild(node);
assert.isTrue(
checks['xml-lang-mismatch'].evaluate.call(checkContext, node)
);
});

it('should return false if lang and xml:lang are not identical', function() {
node.setAttribute('lang', 'en');
node.setAttribute('xml:lang', 'fr-FR');
fixture.appendChild(node);
var actual = checks['xml-lang-mismatch'].evaluate.call(checkContext, node);
assert.isFalse(actual);
});
});
32 changes: 32 additions & 0 deletions test/commons/utils/get-base-lang.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
describe('utils.getBaseLang', function() {
'use strict';

it('returns base lang as peanut for argument peanut-BUTTER', function() {
var actual = axe.commons.utils.getBaseLang('peanut-BUTTER');
assert.equal(actual, 'peanut');
});

it('returns base lang as fr for argument FR-CA', function() {
var actual = axe.commons.utils.getBaseLang('FR-CA');
assert.strictEqual(actual, 'fr');
});

it('returns base lang which is the prefix string before the first - (hyphen)', function() {
var actual = axe.commons.utils.getBaseLang('en-GB');
assert.equal(actual, 'en');
});

it('returns primary language subtag as base lang for multi hyphenated argument', function() {
var actual = axe.commons.utils.getBaseLang('SOME-random-lang');
assert.strictEqual(actual, 'some');
});

it('returns an empty string when argument is null or undefined', function() {
var actualNull = axe.commons.utils.getBaseLang(null);
var actualUndefined = axe.commons.utils.getBaseLang(undefined);
var actualEmpty = axe.commons.utils.getBaseLang();
assert.strictEqual(actualNull, '');
assert.strictEqual(actualUndefined, '');
assert.strictEqual(actualEmpty, '');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
describe('html-xml-lang-mismatch test', function() {
'use strict';

var results;
before(function(done) {
axe.run(
{
runOnly: {
type: 'rule',
values: ['html-xml-lang-mismatch']
}
},
function(err, r) {
assert.isNull(err);
results = r;
done();
}
);
});

describe('violations', function() {
it('should find one', function() {
assert.lengthOf(results.violations[0].nodes, 1);
});

it('should find html', function() {
assert.deepEqual(results.violations[0].nodes[0].target, ['html']);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="fr" xml:lang="en">
<head>
<link rel="stylesheet" type="text/css" href="/node_modules/mocha/mocha.css" />
<script src="/node_modules/mocha/mocha.js"></script>
<script src="/node_modules/chai/chai.js"></script>
<script src="/axe.js"></script>
<script>
mocha.setup({
timeout: 10000,
ui: 'bdd'
});
var assert = chai.assert;
</script>
</head>
<body>
<div id="mocha" role="complementary"></div>
<script src="/test/testutils.js"></script>
<script src="xml-lang-mismatch.fail.js"></script>
<script src="/test/integration/adapter.js"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="fr-CA" xml:lang="en-CA">
<head>
<link rel="stylesheet" type="text/css" href="/node_modules/mocha/mocha.css" />
<script src="/node_modules/mocha/mocha.js"></script>
<script src="/node_modules/chai/chai.js"></script>
<script src="/axe.js"></script>
<script>
mocha.setup({
timeout: 10000,
ui: 'bdd'
});
var assert = chai.assert;
</script>
</head>
<body>
<div id="mocha" role="complementary"></div>
<script src="/test/testutils.js"></script>
<script src="xml-lang-mismatch.fail.js"></script>
<script src="/test/integration/adapter.js"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
describe('html-xml-lang-mismatch test', function() {
'use strict';

var results;
before(function(done) {
axe.run(
{
runOnly: {
type: 'rule',
values: ['html-xml-lang-mismatch']
}
},
function(err, r) {
assert.isNull(err);
results = r;
done();
}
);
});

describe('inapplicable', function() {
it('should find one', function() {
assert.lengthOf(results.inapplicable, 1);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="fr" xml:lang="">
<head>
<link rel="stylesheet" type="text/css" href="/node_modules/mocha/mocha.css" />
<script src="/node_modules/mocha/mocha.js"></script>
<script src="/node_modules/chai/chai.js"></script>
<script src="/axe.js"></script>
<script>
mocha.setup({
timeout: 10000,
ui: 'bdd'
});
var assert = chai.assert;
</script>
</head>
<body>
<div id="mocha" role="complementary"></div>
<script src="/test/testutils.js"></script>
<script src="xml-lang-mismatch.inapplicable.js"></script>
<script src="/test/integration/adapter.js"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="" xml:lang="">
<head>
<link rel="stylesheet" type="text/css" href="/node_modules/mocha/mocha.css" />
<script src="/node_modules/mocha/mocha.js"></script>
<script src="/node_modules/chai/chai.js"></script>
<script src="/axe.js"></script>
<script>
mocha.setup({
timeout: 10000,
ui: 'bdd'
});
var assert = chai.assert;
</script>
</head>
<body>
<div id="mocha" role="complementary"></div>
<script src="/test/testutils.js"></script>
<script src="xml-lang-mismatch.inapplicable.js"></script>
<script src="/test/integration/adapter.js"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
describe('html-xml-lang-mismatch test', function() {
'use strict';

var results;
before(function(done) {
axe.run(
{
runOnly: {
type: 'rule',
values: ['html-xml-lang-mismatch']
}
},
function(err, r) {
assert.isNull(err);
results = r;
done();
}
);
});

describe('passes', function() {
it('should find one', function() {
assert.lengthOf(results.passes[0].nodes, 1);
});

it('should find html', function() {
assert.deepEqual(results.passes[0].nodes[0].target, ['html']);
});
});
});
Loading