Skip to content
This repository was archived by the owner on Dec 7, 2022. It is now read-only.

Commit aa0a0b6

Browse files
author
Alice
committed
Merge pull request #56 from alice/master
Round colour contrast when checking for low contrast; allow configuration of maximum number of results for audit rule
2 parents 3c23a70 + a1b6fe9 commit aa0a0b6

13 files changed

Lines changed: 200 additions & 68 deletions

src/js/AccessibilityUtils.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -820,10 +820,15 @@ axs.utils.isNativeTextElement = function(element) {
820820
* @return {boolean}
821821
*/
822822
axs.utils.isLowContrast = function(contrastRatio, style, opt_strict) {
823-
if (!opt_strict)
824-
return contrastRatio < 3.0 || (!axs.utils.isLargeFont(style) && contrastRatio < 4.5);
825-
else
826-
return contrastRatio < 4.5 || (!axs.utils.isLargeFont(style) && contrastRatio < 7.0);
823+
// Round to nearest 0.1
824+
var roundedContrastRatio = (Math.round(contrastRatio * 10) / 10);
825+
if (!opt_strict) {
826+
return roundedContrastRatio < 3.0 ||
827+
(!axs.utils.isLargeFont(style) && roundedContrastRatio < 4.5);
828+
} else {
829+
return roundedContrastRatio < 4.5 ||
830+
(!axs.utils.isLargeFont(style) && roundedContrastRatio < 7.0);
831+
}
827832
};
828833

829834
/**

src/js/Audit.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ axs.AuditConfiguration = function() {
5757
*/
5858
this.auditRulesToIgnore = null;
5959

60+
/**
61+
* The maximum number of results to collect for each audit rule. If more
62+
* than this number of results is found, 'resultsTruncated' is set to true
63+
* in the result object. If this is null, all results will be returned.
64+
*/
65+
this.maxResults = null;
66+
6067
/**
6168
* Whether this audit run can use the console API.
6269
* @type {boolean}
@@ -166,13 +173,15 @@ axs.Audit.run = function(opt_configuration) {
166173
if (!withConsoleApi && auditRule.requiresConsoleAPI)
167174
continue;
168175

169-
var args = [];
176+
var options = {};
170177
var ignoreSelectors = configuration.getIgnoreSelectors(auditRule.name);
171178
if (ignoreSelectors.length > 0 || configuration.scope)
172-
args.push(ignoreSelectors);
179+
options['ignoreSelectors'] = ignoreSelectors;
173180
if (configuration.scope)
174-
args.push(configuration.scope);
175-
var result = auditRule.run.apply(auditRule, args);
181+
options['scope'] = configuration.scope;
182+
if (configuration.maxResults)
183+
options['maxResults'] = configuration.maxResults;
184+
var result = auditRule.run.call(auditRule, options);
176185
var ruleValues = axs.utils.namedValues(auditRule);
177186
ruleValues.severity = configuration.getSeverity(auditRuleName) ||
178187
ruleValues.severity;

src/js/AuditRule.js

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,22 @@ axs.AuditRule.collectMatchingElements = function(node, matcher, collection) {
133133
}
134134

135135
/**
136-
* @param {Array.<string>=} opt_ignoreSelectors
137-
* @param {Element=} opt_scope The scope in which the element selector should run.
138-
* Defaults to `document`.
139-
* @return {?Object.<string, (axs.constants.AuditResult|?Array.<Element>)>}
136+
* @param {Object} options
137+
* Optional named parameters:
138+
* ignoreSelectors: Selectors for parts of the page to ignore for this rule.
139+
* scope: The scope in which the element selector should run.
140+
* Defaults to `document`.
141+
* maxResults: The maximum number of results to collect. If more than this
142+
* number of results is found, 'resultsTruncated' is set to true in the
143+
* returned object. If this is null or undefined, all results will be
144+
* returned.
145+
* @return {?Object.<string, (axs.constants.AuditResult|?Array.<Element>|boolean)>}
140146
*/
141-
axs.AuditRule.prototype.run = function(opt_ignoreSelectors, opt_scope) {
142-
var ignoreSelectors = opt_ignoreSelectors || [];
143-
var scope = opt_scope || document;
147+
axs.AuditRule.prototype.run = function(options) {
148+
var options = options || {};
149+
var ignoreSelectors = 'ignoreSelectors' in options ? options['ignoreSelectors'] : [];
150+
var scope = 'scope' in options ? options['scope'] : document;
151+
var maxResults = 'maxResults' in options ? options['maxResults'] : null;
144152

145153
var relevantElements = [];
146154
axs.AuditRule.collectMatchingElements(scope, this.relevantElementMatcher_, relevantElements);
@@ -158,13 +166,18 @@ axs.AuditRule.prototype.run = function(opt_ignoreSelectors, opt_scope) {
158166
if (!relevantElements.length)
159167
return { result: axs.constants.AuditResult.NA };
160168
for (var i = 0; i < relevantElements.length; i++) {
169+
if (maxResults != null && failingElements.length >= maxResults)
170+
break;
161171
var element = relevantElements[i];
162172
if (!ignored(element) && this.test_(element))
163173
this.addElement(failingElements, element);
164174
}
165-
166175
var result = failingElements.length ? axs.constants.AuditResult.FAIL : axs.constants.AuditResult.PASS;
167-
return { result: result, elements: failingElements };
176+
var results = { result: result, elements: failingElements};
177+
if (i < relevantElements.length)
178+
results['resultsTruncated'] = true;
179+
180+
return results;
168181
};
169182

170183
axs.AuditRule.specs = {};
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,34 @@
11
module('BadAriaAttributeValue');
22

33
test('Empty idref value is ok', function() {
4-
var fixtures = document.getElementById('qunit-fixture');
4+
var fixture = document.getElementById('qunit-fixture');
55
var div = document.createElement('div');
6-
fixtures.appendChild(div);
6+
fixture.appendChild(div);
77
div.setAttribute('aria-activedescendant', '');
88
deepEqual(
9-
axs.AuditRules.getRule('badAriaAttributeValue').run([], fixtures),
9+
axs.AuditRules.getRule('badAriaAttributeValue').run({ scope: fixture }),
1010
{ elements: [], result: axs.constants.AuditResult.PASS }
1111
);
1212
});
1313

1414
test('Bad number value doesn\'t cause crash', function() {
15-
var fixtures = document.getElementById('qunit-fixture');
15+
var fixture = document.getElementById('qunit-fixture');
1616
var div = document.createElement('div');
17-
fixtures.appendChild(div);
17+
fixture.appendChild(div);
1818
div.setAttribute('aria-valuenow', 'foo');
1919
deepEqual(
20-
axs.AuditRules.getRule('badAriaAttributeValue').run([], fixtures),
20+
axs.AuditRules.getRule('badAriaAttributeValue').run({ scope: fixture }),
2121
{ elements: [div], result: axs.constants.AuditResult.FAIL }
2222
);
2323
});
2424

2525
test('Good number value is good', function() {
26-
var fixtures = document.getElementById('qunit-fixture');
26+
var fixture = document.getElementById('qunit-fixture');
2727
var div = document.createElement('div');
28-
fixtures.appendChild(div);
28+
fixture.appendChild(div);
2929
div.setAttribute('aria-valuenow', '10');
3030
deepEqual(
31-
axs.AuditRules.getRule('badAriaAttributeValue').run([], fixtures),
31+
axs.AuditRules.getRule('badAriaAttributeValue').run({ scope: fixture }),
3232
{ elements: [], result: axs.constants.AuditResult.PASS }
3333
);
3434
});

test/audits/bad-aria-role-test.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,50 @@ module("BadAriaRole");
22

33
test("No elements === no problems.", function() {
44
// Setup fixture
5-
var fixtures = document.getElementById('qunit-fixture');
5+
var fixture = document.getElementById('qunit-fixture');
66
deepEqual(
7-
axs.AuditRules.getRule('badAriaRole').run([], fixtures),
7+
axs.AuditRules.getRule('badAriaRole').run({ scope: fixture }),
88
{ result: axs.constants.AuditResult.NA }
99
);
1010
});
1111

1212
test("No roles === no problems.", function() {
1313
// Setup fixture
14-
var fixtures = document.getElementById('qunit-fixture');
14+
var fixture = document.getElementById('qunit-fixture');
1515
for (var i = 0; i < 10; i++)
16-
fixtures.appendChild(document.createElement('div'));
16+
fixture.appendChild(document.createElement('div'));
1717

1818
deepEqual(
19-
axs.AuditRules.getRule('badAriaRole').run([], fixtures),
19+
axs.AuditRules.getRule('badAriaRole').run({ scope: fixture }),
2020
{ result: axs.constants.AuditResult.NA }
2121
);
2222
});
2323

2424
test("Good role === no problems.", function() {
2525
// Setup fixture
26-
var fixtures = document.getElementById('qunit-fixture');
26+
var fixture = document.getElementById('qunit-fixture');
2727
for (r in axs.constants.ARIA_ROLES) {
2828
if (axs.constants.ARIA_ROLES.hasOwnProperty(r)) {
2929
var div = document.createElement('div');
3030
div.setAttribute('role', r);
31-
fixtures.appendChild(div);
31+
fixture.appendChild(div);
3232
}
3333
}
3434

3535
deepEqual(
36-
axs.AuditRules.getRule('badAriaRole').run([], fixtures),
36+
axs.AuditRules.getRule('badAriaRole').run({ scope: fixture }),
3737
{ elements: [], result: axs.constants.AuditResult.PASS }
3838
);
3939
});
4040

4141
test("Bad role == problem", function() {
4242
// Setup fixture
43-
var fixtures = document.getElementById('qunit-fixture');
43+
var fixture = document.getElementById('qunit-fixture');
4444
var div = document.createElement('div');
4545
div.setAttribute('role', 'not-an-aria-role');
46-
fixtures.appendChild(div);
46+
fixture.appendChild(div);
4747
deepEqual(
48-
axs.AuditRules.getRule('badAriaRole').run([], fixtures),
48+
axs.AuditRules.getRule('badAriaRole').run({ scope: fixture }),
4949
{ elements: [div], result: axs.constants.AuditResult.FAIL }
5050
);
5151

test/audits/controls-without-label-test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ test('Button with type="submit" or type="reset" has label', function() {
1212
fixture.appendChild(resetInput);
1313

1414
var rule = axs.AuditRules.getRule('controlsWithoutLabel');
15-
equal(rule.run([], fixture).result,
15+
equal(rule.run({scope: fixture}).result,
1616
axs.constants.AuditResult.PASS);
1717
});
1818

@@ -25,7 +25,7 @@ test('Button element with inner text needs no label', function() {
2525
fixture.appendChild(button);
2626

2727
var rule = axs.AuditRules.getRule('controlsWithoutLabel');
28-
equal(rule.run([], fixture).result,
28+
equal(rule.run({ scope: fixture }).result,
2929
axs.constants.AuditResult.PASS);
3030
});
3131

@@ -38,7 +38,7 @@ test('Button element with empty inner text does need a label', function() {
3838
fixture.appendChild(button);
3939

4040
var rule = axs.AuditRules.getRule('controlsWithoutLabel');
41-
equal(rule.run([], fixture).result,
41+
equal(rule.run({ scope: fixture }).result,
4242
axs.constants.AuditResult.FAIL);
4343
});
4444

@@ -52,6 +52,6 @@ test('Input type button with value needs no label', function() {
5252
fixture.appendChild(input);
5353

5454
var rule = axs.AuditRules.getRule('controlsWithoutLabel');
55-
equal(rule.run([], fixture).result,
55+
equal(rule.run({ scope: fixture }).result,
5656
axs.constants.AuditResult.PASS);
5757
});

test/audits/low-contrast-test.js

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,65 @@
11
module("LowContrast");
22

3+
test("No text = no relevant elements", function() {
4+
var fixture = document.getElementById('qunit-fixture');
5+
var div = document.createElement('div');
6+
div.style.backgroundColor = 'white';
7+
div.style.color = 'white';
8+
fixture.appendChild(div);
9+
deepEqual(
10+
axs.AuditRules.getRule('lowContrastElements').run({ scope: fixture }),
11+
{ result: axs.constants.AuditResult.NA }
12+
);
13+
});
14+
15+
test("Black on white = no problem", function() {
16+
var fixture = document.getElementById('qunit-fixture');
17+
var div = document.createElement('div');
18+
div.style.backgroundColor = 'white';
19+
div.style.color = 'black';
20+
div.textContent = 'Some text';
21+
fixture.appendChild(div);
22+
deepEqual(
23+
axs.AuditRules.getRule('lowContrastElements').run({ scope: fixture }),
24+
{ elements: [], result: axs.constants.AuditResult.PASS }
25+
);
26+
});
27+
28+
test("Low contrast = fail", function() {
29+
var fixture = document.getElementById('qunit-fixture');
30+
var div = document.createElement('div');
31+
div.style.backgroundColor = 'white';
32+
div.style.color = '#aaa'; // Contrast ratio = 2.32
33+
div.textContent = 'Some text';
34+
fixture.appendChild(div);
35+
deepEqual(
36+
axs.AuditRules.getRule('lowContrastElements').run({ scope: fixture }),
37+
{ elements: [div], result: axs.constants.AuditResult.FAIL }
38+
);
39+
});
40+
341
test("Opacity is handled", function() {
442
// Setup fixture
5-
var fixtures = document.getElementById('qunit-fixture');
43+
var fixture = document.getElementById('qunit-fixture');
644
var elementWithOpacity = document.createElement('div');
745
elementWithOpacity.style.opacity = '0.4';
846
elementWithOpacity.textContent = 'Some text';
9-
fixtures.appendChild(elementWithOpacity);
47+
fixture.appendChild(elementWithOpacity);
1048
deepEqual(
11-
axs.AuditRules.getRule('lowContrastElements').run([], fixtures),
49+
axs.AuditRules.getRule('lowContrastElements').run({ scope: fixture }),
1250
{ elements: [elementWithOpacity], result: axs.constants.AuditResult.FAIL }
1351
);
1452
});
53+
54+
test("Uses tolerance value", function() {
55+
var fixture = document.getElementById('qunit-fixture');
56+
var div = document.createElement('div');
57+
div.style.backgroundColor = 'white';
58+
div.style.color = '#777'; // Contrast ratio = 4.48
59+
div.textContent = 'Some text';
60+
fixture.appendChild(div);
61+
deepEqual(
62+
axs.AuditRules.getRule('lowContrastElements').run({ scope: fixture }),
63+
{ elements: [], result: axs.constants.AuditResult.PASS }
64+
);
65+
});

test/audits/main-role-on-inappropriate-element-test.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ test('No role=main -> no relevant elements', function() {
2020
fixture.appendChild(div);
2121

2222
deepEqual(
23-
axs.AuditRules.getRule('mainRoleOnInappropriateElement').run([], fixture),
23+
axs.AuditRules.getRule('mainRoleOnInappropriateElement').run({ scope: fixture }),
2424
{ result: axs.constants.AuditResult.NA }
2525
);
2626
});
@@ -32,7 +32,7 @@ test('role=main on empty element === fail', function() {
3232
fixture.appendChild(div);
3333

3434
deepEqual(
35-
axs.AuditRules.getRule('mainRoleOnInappropriateElement').run([], fixture),
35+
axs.AuditRules.getRule('mainRoleOnInappropriateElement').run({ scope: fixture }),
3636
{ elements: [div], result: axs.constants.AuditResult.FAIL }
3737
);
3838
});
@@ -45,7 +45,7 @@ test('role=main on element with textContent < 50 characters === pass', function(
4545
fixture.appendChild(div);
4646

4747
deepEqual(
48-
axs.AuditRules.getRule('mainRoleOnInappropriateElement').run([], fixture),
48+
axs.AuditRules.getRule('mainRoleOnInappropriateElement').run({ scope: fixture }),
4949
{ elements: [div], result: axs.constants.AuditResult.FAIL }
5050
);
5151
});
@@ -58,7 +58,7 @@ test('role=main on element with textContent >= 50 characters === pass', function
5858
fixture.appendChild(div);
5959

6060
deepEqual(
61-
axs.AuditRules.getRule('mainRoleOnInappropriateElement').run([], fixture),
61+
axs.AuditRules.getRule('mainRoleOnInappropriateElement').run({ scope: fixture }),
6262
{ elements: [], result: axs.constants.AuditResult.PASS }
6363
);
6464
});
@@ -71,6 +71,6 @@ test('role=main on inline element === fail', function() {
7171
fixture.appendChild(span);
7272

7373
deepEqual(
74-
axs.AuditRules.getRule('mainRoleOnInappropriateElement').run([], fixture),
74+
axs.AuditRules.getRule('mainRoleOnInappropriateElement').run({ scope: fixture }),
7575
{ elements: [span], result: axs.constants.AuditResult.FAIL });
7676
});

0 commit comments

Comments
 (0)