-
Notifications
You must be signed in to change notification settings - Fork 779
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
Changes from 13 commits
04022cb
4120f3c
f0ea8ff
3541c90
05e3544
c909c50
30fcefc
0d471c8
1c50f83
76b9515
8cb1aab
fbf6a54
57c66b7
31e37e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"id": "xml-lang-mismatch", | ||
This comment was marked as outdated.
Sorry, something went wrong.
This comment was marked as resolved.
Sorry, something went wrong. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
} | ||
} | ||
} |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorted! |
||
if (!lang) { | ||
return ''; | ||
} | ||
return lang | ||
.trim() | ||
.split('-')[0] | ||
.toLowerCase(); | ||
}; |
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": [] | ||
} |
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) | ||
); |
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); | ||
}); | ||
}); |
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']); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
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.