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

Commit

Permalink
Merge pull request #56 from alice/master
Browse files Browse the repository at this point in the history
Round colour contrast when checking for low contrast; allow configuration of maximum number of results for audit rule
  • Loading branch information
Alice committed Oct 3, 2013
2 parents 3c23a70 + a1b6fe9 commit aa0a0b6
Show file tree
Hide file tree
Showing 13 changed files with 200 additions and 68 deletions.
13 changes: 9 additions & 4 deletions src/js/AccessibilityUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -820,10 +820,15 @@ axs.utils.isNativeTextElement = function(element) {
* @return {boolean}
*/
axs.utils.isLowContrast = function(contrastRatio, style, opt_strict) {
if (!opt_strict)
return contrastRatio < 3.0 || (!axs.utils.isLargeFont(style) && contrastRatio < 4.5);
else
return contrastRatio < 4.5 || (!axs.utils.isLargeFont(style) && contrastRatio < 7.0);
// Round to nearest 0.1
var roundedContrastRatio = (Math.round(contrastRatio * 10) / 10);
if (!opt_strict) {
return roundedContrastRatio < 3.0 ||
(!axs.utils.isLargeFont(style) && roundedContrastRatio < 4.5);
} else {
return roundedContrastRatio < 4.5 ||
(!axs.utils.isLargeFont(style) && roundedContrastRatio < 7.0);
}
};

/**
Expand Down
17 changes: 13 additions & 4 deletions src/js/Audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ axs.AuditConfiguration = function() {
*/
this.auditRulesToIgnore = null;

/**
* The maximum number of results to collect for each audit rule. If more
* than this number of results is found, 'resultsTruncated' is set to true
* in the result object. If this is null, all results will be returned.
*/
this.maxResults = null;

/**
* Whether this audit run can use the console API.
* @type {boolean}
Expand Down Expand Up @@ -166,13 +173,15 @@ axs.Audit.run = function(opt_configuration) {
if (!withConsoleApi && auditRule.requiresConsoleAPI)
continue;

var args = [];
var options = {};
var ignoreSelectors = configuration.getIgnoreSelectors(auditRule.name);
if (ignoreSelectors.length > 0 || configuration.scope)
args.push(ignoreSelectors);
options['ignoreSelectors'] = ignoreSelectors;
if (configuration.scope)
args.push(configuration.scope);
var result = auditRule.run.apply(auditRule, args);
options['scope'] = configuration.scope;
if (configuration.maxResults)
options['maxResults'] = configuration.maxResults;
var result = auditRule.run.call(auditRule, options);
var ruleValues = axs.utils.namedValues(auditRule);
ruleValues.severity = configuration.getSeverity(auditRuleName) ||
ruleValues.severity;
Expand Down
31 changes: 22 additions & 9 deletions src/js/AuditRule.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,22 @@ axs.AuditRule.collectMatchingElements = function(node, matcher, collection) {
}

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

var relevantElements = [];
axs.AuditRule.collectMatchingElements(scope, this.relevantElementMatcher_, relevantElements);
Expand All @@ -158,13 +166,18 @@ axs.AuditRule.prototype.run = function(opt_ignoreSelectors, opt_scope) {
if (!relevantElements.length)
return { result: axs.constants.AuditResult.NA };
for (var i = 0; i < relevantElements.length; i++) {
if (maxResults != null && failingElements.length >= maxResults)
break;
var element = relevantElements[i];
if (!ignored(element) && this.test_(element))
this.addElement(failingElements, element);
}

var result = failingElements.length ? axs.constants.AuditResult.FAIL : axs.constants.AuditResult.PASS;
return { result: result, elements: failingElements };
var results = { result: result, elements: failingElements};
if (i < relevantElements.length)
results['resultsTruncated'] = true;

return results;
};

axs.AuditRule.specs = {};
18 changes: 9 additions & 9 deletions test/audits/bad-aria-attribute-value-test.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
module('BadAriaAttributeValue');

test('Empty idref value is ok', function() {
var fixtures = document.getElementById('qunit-fixture');
var fixture = document.getElementById('qunit-fixture');
var div = document.createElement('div');
fixtures.appendChild(div);
fixture.appendChild(div);
div.setAttribute('aria-activedescendant', '');
deepEqual(
axs.AuditRules.getRule('badAriaAttributeValue').run([], fixtures),
axs.AuditRules.getRule('badAriaAttributeValue').run({ scope: fixture }),
{ elements: [], result: axs.constants.AuditResult.PASS }
);
});

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

test('Good number value is good', function() {
var fixtures = document.getElementById('qunit-fixture');
var fixture = document.getElementById('qunit-fixture');
var div = document.createElement('div');
fixtures.appendChild(div);
fixture.appendChild(div);
div.setAttribute('aria-valuenow', '10');
deepEqual(
axs.AuditRules.getRule('badAriaAttributeValue').run([], fixtures),
axs.AuditRules.getRule('badAriaAttributeValue').run({ scope: fixture }),
{ elements: [], result: axs.constants.AuditResult.PASS }
);
});
22 changes: 11 additions & 11 deletions test/audits/bad-aria-role-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,50 @@ module("BadAriaRole");

test("No elements === no problems.", function() {
// Setup fixture
var fixtures = document.getElementById('qunit-fixture');
var fixture = document.getElementById('qunit-fixture');
deepEqual(
axs.AuditRules.getRule('badAriaRole').run([], fixtures),
axs.AuditRules.getRule('badAriaRole').run({ scope: fixture }),
{ result: axs.constants.AuditResult.NA }
);
});

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

deepEqual(
axs.AuditRules.getRule('badAriaRole').run([], fixtures),
axs.AuditRules.getRule('badAriaRole').run({ scope: fixture }),
{ result: axs.constants.AuditResult.NA }
);
});

test("Good role === no problems.", function() {
// Setup fixture
var fixtures = document.getElementById('qunit-fixture');
var fixture = document.getElementById('qunit-fixture');
for (r in axs.constants.ARIA_ROLES) {
if (axs.constants.ARIA_ROLES.hasOwnProperty(r)) {
var div = document.createElement('div');
div.setAttribute('role', r);
fixtures.appendChild(div);
fixture.appendChild(div);
}
}

deepEqual(
axs.AuditRules.getRule('badAriaRole').run([], fixtures),
axs.AuditRules.getRule('badAriaRole').run({ scope: fixture }),
{ elements: [], result: axs.constants.AuditResult.PASS }
);
});

test("Bad role == problem", function() {
// Setup fixture
var fixtures = document.getElementById('qunit-fixture');
var fixture = document.getElementById('qunit-fixture');
var div = document.createElement('div');
div.setAttribute('role', 'not-an-aria-role');
fixtures.appendChild(div);
fixture.appendChild(div);
deepEqual(
axs.AuditRules.getRule('badAriaRole').run([], fixtures),
axs.AuditRules.getRule('badAriaRole').run({ scope: fixture }),
{ elements: [div], result: axs.constants.AuditResult.FAIL }
);

Expand Down
8 changes: 4 additions & 4 deletions test/audits/controls-without-label-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ test('Button with type="submit" or type="reset" has label', function() {
fixture.appendChild(resetInput);

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

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

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

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

var rule = axs.AuditRules.getRule('controlsWithoutLabel');
equal(rule.run([], fixture).result,
equal(rule.run({ scope: fixture }).result,
axs.constants.AuditResult.FAIL);
});

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

var rule = axs.AuditRules.getRule('controlsWithoutLabel');
equal(rule.run([], fixture).result,
equal(rule.run({ scope: fixture }).result,
axs.constants.AuditResult.PASS);
});
57 changes: 54 additions & 3 deletions test/audits/low-contrast-test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,65 @@
module("LowContrast");

test("No text = no relevant elements", function() {
var fixture = document.getElementById('qunit-fixture');
var div = document.createElement('div');
div.style.backgroundColor = 'white';
div.style.color = 'white';
fixture.appendChild(div);
deepEqual(
axs.AuditRules.getRule('lowContrastElements').run({ scope: fixture }),
{ result: axs.constants.AuditResult.NA }
);
});

test("Black on white = no problem", function() {
var fixture = document.getElementById('qunit-fixture');
var div = document.createElement('div');
div.style.backgroundColor = 'white';
div.style.color = 'black';
div.textContent = 'Some text';
fixture.appendChild(div);
deepEqual(
axs.AuditRules.getRule('lowContrastElements').run({ scope: fixture }),
{ elements: [], result: axs.constants.AuditResult.PASS }
);
});

test("Low contrast = fail", function() {
var fixture = document.getElementById('qunit-fixture');
var div = document.createElement('div');
div.style.backgroundColor = 'white';
div.style.color = '#aaa'; // Contrast ratio = 2.32
div.textContent = 'Some text';
fixture.appendChild(div);
deepEqual(
axs.AuditRules.getRule('lowContrastElements').run({ scope: fixture }),
{ elements: [div], result: axs.constants.AuditResult.FAIL }
);
});

test("Opacity is handled", function() {
// Setup fixture
var fixtures = document.getElementById('qunit-fixture');
var fixture = document.getElementById('qunit-fixture');
var elementWithOpacity = document.createElement('div');
elementWithOpacity.style.opacity = '0.4';
elementWithOpacity.textContent = 'Some text';
fixtures.appendChild(elementWithOpacity);
fixture.appendChild(elementWithOpacity);
deepEqual(
axs.AuditRules.getRule('lowContrastElements').run([], fixtures),
axs.AuditRules.getRule('lowContrastElements').run({ scope: fixture }),
{ elements: [elementWithOpacity], result: axs.constants.AuditResult.FAIL }
);
});

test("Uses tolerance value", function() {
var fixture = document.getElementById('qunit-fixture');
var div = document.createElement('div');
div.style.backgroundColor = 'white';
div.style.color = '#777'; // Contrast ratio = 4.48
div.textContent = 'Some text';
fixture.appendChild(div);
deepEqual(
axs.AuditRules.getRule('lowContrastElements').run({ scope: fixture }),
{ elements: [], result: axs.constants.AuditResult.PASS }
);
});
10 changes: 5 additions & 5 deletions test/audits/main-role-on-inappropriate-element-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ test('No role=main -> no relevant elements', function() {
fixture.appendChild(div);

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

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

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

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

deepEqual(
axs.AuditRules.getRule('mainRoleOnInappropriateElement').run([], fixture),
axs.AuditRules.getRule('mainRoleOnInappropriateElement').run({ scope: fixture }),
{ elements: [span], result: axs.constants.AuditResult.FAIL });
});
Loading

0 comments on commit aa0a0b6

Please sign in to comment.