blob: 31acd756628d3da4fab15f742c1fb30a3dea355f [file] [edit]
// Copyright 2012 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.require('axs.AuditResults');
goog.require('axs.AuditRules');
goog.require('axs.utils');
goog.provide('axs.Audit');
goog.provide('axs.AuditConfiguration');
/**
* Object to hold configuration for an Audit run.
* @constructor
* @param {?Object=} config Configuration object
* The following configuration options are supported:
* - scope
* - auditRulesToRun
* - auditRulesToIgnore
* - maxResults
* - withConsoleApi
* - showUnsupportedRulesWarning
*/
axs.AuditConfiguration = function(config) {
if (config == null) {
config = {};
}
/**
* Dictionary of { audit rule name : { rules } } where rules is a dictionary
* of { rule type : value }.
* Possible rule types:
* - ignore: value is a list of CSS selectors representing parts of the page
* which can be ignored for this audit rule.
* - config: value is an object containing configuration for this audit
* rule. It will be passed to the test() method.
* @type {Object}
* @private
*/
this.rules_ = {};
/**
* The "start point" for the audit: the element which contains the portion of
* the page which should be audited.
* If null, the document will be used as the scope.
* @type {?Element}
*/
this.scope = null;
/**
* A list of rule names representing the audit rules to be run. If this is
* empty or |null|, all audit rules will be run.
* @type {Array.<String>}
*/
this.auditRulesToRun = null;
/**
* A list of rule names representing the audit rules which should not be run.
* If this is empty or |null|, all audit rules will be run.
* @type {Array.<String>}
*/
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}
*/
this.withConsoleApi = false;
/**
* Do we want to show a warning that there are audit rules which are not supported in this configuration?
* @type {boolean}
*/
this.showUnsupportedRulesWarning = true;
for (var prop in this) {
if ((this.hasOwnProperty(prop)) && (prop in config)) {
this[prop] = config[prop];
}
}
goog.exportProperty(this, 'scope', this.scope);
goog.exportProperty(this, 'auditRulesToRun', this.auditRulesToRun);
goog.exportProperty(this, 'auditRulesToIgnore', this.auditRulesToIgnore);
goog.exportProperty(this, 'withConsoleApi', this.withConsoleApi);
goog.exportProperty(this, 'showUnsupportedRulesWarning', this.showUnsupportedRulesWarning);
};
goog.exportSymbol('axs.AuditConfiguration', axs.AuditConfiguration);
axs.AuditConfiguration.prototype = {
/**
* Add the given selectors to the ignore list for the given audit rule.
* @param {string} auditRuleName The name of the audit rule
* @param {Array.<string>} selectors Query selectors to match nodes to
* ignore
*/
ignoreSelectors: function(auditRuleName, selectors) {
if (!(auditRuleName in this.rules_))
this.rules_[auditRuleName] = {};
if (!('ignore' in this.rules_[auditRuleName]))
this.rules_[auditRuleName].ignore = [];
Array.prototype.push.call(this.rules_[auditRuleName].ignore, selectors);
},
/**
* Gets the selectors which have been added to the ignore list for the given
* audit rule.
* @param {string} auditRuleName The name of the audit rule
* @return {Array.<string>} A list of query selector strings which match nodes
* to be ignored for the given rule.
*/
getIgnoreSelectors: function(auditRuleName) {
if ((auditRuleName in this.rules_) &&
('ignore' in this.rules_[auditRuleName])) {
return this.rules_[auditRuleName].ignore;
}
return [];
},
/**
* Sets the user-specified severity for the given audit rule. This will
* replace the default severity for that audit rule in the audit results.
* @param {string} auditRuleName
* @param {axs.constants.Severity} severity
*/
setSeverity: function(auditRuleName, severity) {
if (!(auditRuleName in this.rules_))
this.rules_[auditRuleName] = {};
this.rules_[auditRuleName].severity = severity;
},
getSeverity: function(auditRuleName) {
if (!(auditRuleName in this.rules_))
return null;
if (!('severity' in this.rules_[auditRuleName]))
return null;
return this.rules_[auditRuleName].severity;
},
/**
* Sets the user-specified configuration for the given audit rule. This will
* vary in structure from rule to rule; see individual rules for
* configuration options.
* @param {string} auditRuleName
* @param {Object} config
*/
setRuleConfig: function(auditRuleName, config) {
if (!(auditRuleName in this.rules_))
this.rules_[auditRuleName] = {};
this.rules_[auditRuleName].config = config;
},
/**
* Gets the user-specified configuration for the given audit rule.
* @param {string} auditRuleName
* @return {Object?} The configuration object for the given audit rule.
*/
getRuleConfig: function(auditRuleName) {
if (!(auditRuleName in this.rules_))
return null;
if (!('config' in this.rules_[auditRuleName]))
return null;
return this.rules_[auditRuleName].config;
}
};
goog.exportProperty(axs.AuditConfiguration.prototype, 'ignoreSelectors',
axs.AuditConfiguration.prototype.ignoreSelectors);
goog.exportProperty(axs.AuditConfiguration.prototype, 'getIgnoreSelectors',
axs.AuditConfiguration.prototype.getIgnoreSelectors);
axs.Audit.unsupportedRulesWarningShown = false;
/**
* Returns the rules that cannot run.
* For example, if the current configuration requires the console API, these
* consist of the rules that require it.
* @param {axs.AuditConfiguration=} opt_configuration
* @return {Array.<String>} A list of rules that cannot be run
*/
axs.Audit.getRulesCannotRun = function(opt_configuration) {
if (opt_configuration.withConsoleApi) {
return [];
}
return axs.AuditRules.getRules().filter(function(rule) {
return rule.requiresConsoleAPI;
}).map(function(rule) {
return rule.code;
});
};
/**
* Runs an audit with all of the audit rules.
* @param {axs.AuditConfiguration=} opt_configuration
* @return {Array.<Object>} Array of Object:
* {
* result, // @type {axs.constants.AuditResult}
* elements, // @type {Array.<Element>}
* rule // @type {axs.AuditRule} - data only (name, severity, code)
* }
*/
axs.Audit.run = function(opt_configuration) {
var configuration = opt_configuration || new axs.AuditConfiguration();
var withConsoleApi = configuration.withConsoleApi;
var results = [];
var auditRules;
if (configuration.auditRulesToRun &&
configuration.auditRulesToRun.length > 0) {
auditRules = configuration.auditRulesToRun;
} else
auditRules = axs.AuditRules.getRules(true);
if (configuration.auditRulesToIgnore) {
for (var i = 0; i < configuration.auditRulesToIgnore.length; i++) {
var auditRuleToIgnore = configuration.auditRulesToIgnore[i];
if (auditRules.indexOf(auditRuleToIgnore) < 0)
continue;
auditRules.splice(auditRules.indexOf(auditRuleToIgnore), 1);
}
}
if (!axs.Audit.unsupportedRulesWarningShown && configuration.showUnsupportedRulesWarning) {
var unsupportedRules = axs.Audit.getRulesCannotRun(configuration);
if (unsupportedRules.length > 0) {
console.warn("Some rules cannot be checked using the axs.Audit.run() method call. Use the Chrome plugin to check these rules: " + unsupportedRules.join(", "));
console.warn("To remove this message, pass an AuditConfiguration object to axs.Audit.run() and set configuration.showUnsupportedRulesWarning = false.");
}
axs.Audit.unsupportedRulesWarningShown = true;
}
for (var i = 0; i < auditRules.length; i++) {
var auditRuleName = auditRules[i];
var auditRule = axs.AuditRules.getRule(auditRuleName);
if (!auditRule)
continue; // Shouldn't happen, but fail silently if it does.
if (auditRule.disabled)
continue;
if (!withConsoleApi && auditRule.requiresConsoleAPI)
continue;
var options = {};
var ignoreSelectors = configuration.getIgnoreSelectors(auditRule.name);
if (ignoreSelectors.length > 0 || configuration.scope)
options['ignoreSelectors'] = ignoreSelectors;
var ruleConfig = configuration.getRuleConfig(auditRule.name);
if (ruleConfig != null)
options['config' ] = ruleConfig;
if (configuration.scope)
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;
result.rule = ruleValues;
results.push(result);
}
return results;
};
goog.exportSymbol('axs.Audit.run', axs.Audit.run);
/**
* Create an AuditResults object citing failures and warnings, for use in
* continuous builds.
* @param {Array.<Object>} results The results returned from the audit run.
* @return {axs.AuditResults} a report of the audit results.
*/
axs.Audit.auditResults = function(results) {
var auditResults = new axs.AuditResults();
for (var i = 0; i < results.length; i++) {
var result = results[i];
if (result.result != axs.constants.AuditResult.FAIL)
continue;
if (result.rule.severity == axs.constants.Severity.SEVERE) {
auditResults.addError(
axs.Audit.accessibilityErrorMessage(result));
} else {
auditResults.addWarning(
axs.Audit.accessibilityErrorMessage(result));
}
}
return auditResults;
};
goog.exportSymbol('axs.Audit.auditResults', axs.Audit.auditResults);
/**
* Create a report based on the results of an Audit.
* @param {Array.<Object>} results The results returned from axs.Audit.run();
* @param {?string} opt_url A URL to visit for more information.
* @return {string} A report of the audit results.
*/
axs.Audit.createReport = function(results, opt_url) {
var message = '*** Begin accessibility audit results ***';
message += '\nAn accessibility audit found ';
message += axs.Audit.auditResults(results).toString();
if (opt_url) {
message += '\nFor more information, please see ' ;
message += opt_url;
}
message += '\n*** End accessibility audit results ***';
return message;
};
goog.exportSymbol('axs.Audit.createReport', axs.Audit.createReport);
/**
* Creates an error message for a given accessibility audit result object.
* @param {Object.<string, (string|Array.<Element>)>} result The result
* object returned from the audit.
* @return {string} An error message describing the failure and listing the
* query selectors for up to five elements which failed the audit rule.
*/
axs.Audit.accessibilityErrorMessage = function(result) {
if (result.rule.severity == axs.constants.Severity.SEVERE)
var message = 'Error: ';
else
var message = 'Warning: ';
message += result.rule.code + ' (' + result.rule.heading +
') failed on the following ' +
(result.elements.length == 1 ? 'element' : 'elements');
if (result.elements.length == 1)
message += ':';
else {
message += ' (1 - ' + Math.min(5, result.elements.length) +
' of ' + result.elements.length + '):';
}
var maxElements = Math.min(result.elements.length, 5);
for (var i = 0; i < maxElements; i++) {
var element = result.elements[i];
message += '\n';
// Get query selector not browser independent. catch any errors and
// default to simple tagName.
try {
message += axs.utils.getQuerySelectorText(element);
} catch (err) {
message += ' tagName:' + element.tagName;
message += ' id:' + element.id;
}
}
if (result.rule.url != '')
message += '\nSee ' + result.rule.url + ' for more information.';
return message;
};
goog.exportSymbol('axs.Audit.accessibilityErrorMessage', axs.Audit.accessibilityErrorMessage);