UNPKG

axe-core

Version:

Accessibility engine for automated Web UI testing

342 lines (299 loc) • 9.35 kB
/*global Rule, Check, RuleResult, commons: true */ /*eslint no-unused-vars: 0*/ function getDefaultConfiguration(audit) { 'use strict'; var config; if (audit) { config = axe.utils.clone(audit); // Commons are configured into axe like everything else, // however because things go funky if we have multiple commons objects // we're not using the copy of that. config.commons = audit.commons; } else { config = {}; } config.reporter = config.reporter || null; config.noHtml = config.noHtml || false; config.rules = config.rules || []; config.checks = config.checks || []; config.data = Object.assign({ checks: {}, rules: {} }, config.data); return config; } function unpackToObject(collection, audit, method) { 'use strict'; var i, l; for (i = 0, l = collection.length; i < l; i++) { audit[method](collection[i]); } } /** * Constructor which holds configured rules and information about the document under test */ function Audit(audit) { // defaults this.brand = 'axe'; this.application = 'axeAPI'; this.tagExclude = ['experimental']; this.defaultConfig = audit; this._init(); } /** * Initializes the rules and checks */ Audit.prototype._init = function () { var audit = getDefaultConfiguration(this.defaultConfig); axe.commons = commons = audit.commons; this.reporter = audit.reporter; this.commands = {}; this.rules = []; this.checks = {}; this.noHtml = audit.noHtml; unpackToObject(audit.rules, this, 'addRule'); unpackToObject(audit.checks, this, 'addCheck'); this.data = {}; this.data.checks = (audit.data && audit.data.checks) || {}; this.data.rules = (audit.data && audit.data.rules) || {}; this.data.failureSummaries = (audit.data && audit.data.failureSummaries) || {}; this.data.incompleteFallbackMessage = (audit.data && audit.data.incompleteFallbackMessage || ''); this._constructHelpUrls(); // create default helpUrls }; /** * Adds a new command to the audit */ Audit.prototype.registerCommand = function (command) { 'use strict'; this.commands[command.id] = command.callback; }; /** * Adds a new rule to the Audit. If a rule with specified ID already exists, it will be overridden * @param {Object} spec Rule specification object */ Audit.prototype.addRule = function (spec) { 'use strict'; if (spec.metadata) { this.data.rules[spec.id] = spec.metadata; } let rule = this.getRule(spec.id); if (rule) { rule.configure(spec); } else { this.rules.push(new Rule(spec, this)); } }; /** * Adds a new check to the Audit. If a Check with specified ID already exists, it will be * reconfigured * * @param {Object} spec Check specification object */ Audit.prototype.addCheck = function (spec) { /*eslint no-eval: 0 */ 'use strict'; let metadata = spec.metadata; if (typeof metadata === 'object') { this.data.checks[spec.id] = metadata; // Transform messages into functions: if (typeof metadata.messages === 'object') { Object.keys(metadata.messages) .filter( prop => metadata.messages.hasOwnProperty(prop) && typeof metadata.messages[prop] === 'string' ).forEach( prop => { if (metadata.messages[prop].indexOf('function') === 0) { metadata.messages[prop] = (new Function('return ' + metadata.messages[prop] + ';'))(); } }); } } if (this.checks[spec.id]) { this.checks[spec.id].configure(spec); } else { this.checks[spec.id] = new Check(spec); } }; /** * Runs the Audit; which in turn should call `run` on each rule. * @async * @param {Context} context The scope definition/context for analysis (include/exclude) * @param {Object} options Options object to pass into rules and/or disable rules or checks * @param {Function} fn Callback function to fire when audit is complete */ Audit.prototype.run = function (context, options, resolve, reject) { 'use strict'; this.normalizeOptions(options); axe._selectCache = []; var q = axe.utils.queue(); this.rules.forEach(function (rule) { if (axe.utils.ruleShouldRun(rule, context, options)) { if (options.performanceTimer) { var markEnd = 'mark_rule_end_' + rule.id; var markStart = 'mark_rule_start_' + rule.id; axe.utils.performanceTimer.mark(markStart); } q.defer(function (res, rej) { rule.run(context, options, function(out) { if (options.performanceTimer) { axe.utils.performanceTimer.mark(markEnd); axe.utils.performanceTimer.measure('rule_'+rule.id, markStart, markEnd); } res(out); }, function (err) { if (!options.debug) { var errResult = Object.assign(new RuleResult(rule), { result: axe.constants.CANTTELL, description: 'An error occured while running this rule', message: err.message, stack: err.stack, error: err }); res(errResult); } else { rej(err); } }); }); } }); q.then(function (results) { axe._selectCache = undefined; // remove the cache resolve(results.filter(function (result) { return !!result; })); }).catch(reject); }; /** * Runs Rule `after` post processing functions * @param {Array} results Array of RuleResults to postprocess * @param {Mixed} options Options object to pass into rules and/or disable rules or checks */ Audit.prototype.after = function (results, options) { 'use strict'; var rules = this.rules; return results.map(function (ruleResult) { var rule = axe.utils.findBy(rules, 'id', ruleResult.id); if (!rule) { // If you see this, you're probably running the Mocha tests with the aXe extension installed throw new Error('Result for unknown rule. You may be running mismatch aXe-core versions'); } return rule.after(ruleResult, options); }); }; /** * Get the rule with a given ID * @param {string} * @return {Rule} */ Audit.prototype.getRule = function (ruleId) { return this.rules.find(rule => rule.id === ruleId); }; /** * Ensure all rules that are expected to run exist * @throws {Error} If any tag or rule specified in options is unknown * @param {Object} options Options object * @return {Object} Validated options object */ Audit.prototype.normalizeOptions = function (options) { /* eslint max-statements: ["error", 22] */ 'use strict'; var audit = this; // Validate runOnly if (typeof options.runOnly === 'object') { if (Array.isArray(options.runOnly)) { options.runOnly = { type: 'tag', values: options.runOnly }; } const only = options.runOnly; if (only.value && !only.values) { only.values = only.value; delete only.value; } if (!Array.isArray(only.values) || only.values.length === 0) { throw new Error('runOnly.values must be a non-empty array'); } // Check if every value in options.runOnly is a known rule ID if (['rule', 'rules'].includes(only.type)) { only.type = 'rule'; only.values.forEach(function (ruleId) { if (!audit.getRule(ruleId)) { throw new Error('unknown rule `' + ruleId + '` in options.runOnly'); } }); // Validate 'tags' (e.g. anything not 'rule') } else if (['tag', 'tags', undefined].includes(only.type)) { only.type = 'tag'; const unmatchedTags = audit.rules.reduce((unmatchedTags, rule) => { return (unmatchedTags.length ? unmatchedTags.filter(tag => !rule.tags.includes(tag)) : unmatchedTags ); }, only.values); if (unmatchedTags.length !== 0) { throw new Error('Could not find tags `' + unmatchedTags.join('`, `') + '`'); } } else { throw new Error(`Unknown runOnly type '${only.type}'`); } } if (typeof options.rules === 'object') { Object.keys(options.rules) .forEach(function (ruleId) { if (!audit.getRule(ruleId)) { throw new Error('unknown rule `' + ruleId + '` in options.rules'); } }); } return options; }; /* * Updates the default options and then applies them * @param {Mixed} options Options object */ Audit.prototype.setBranding = function (branding) { 'use strict'; let previous = { brand: this.brand, application: this.application }; if (branding && branding.hasOwnProperty('brand') && branding.brand && typeof branding.brand === 'string') { this.brand = branding.brand; } if (branding && branding.hasOwnProperty('application') && branding.application && typeof branding.application === 'string') { this.application = branding.application; } this._constructHelpUrls(previous); }; /** * For all the rules, create the helpUrl and add it to the data for that rule */ function getHelpUrl ({brand, application}, ruleId, version) { return axe.constants.helpUrlBase + brand + '/' + ( version || axe.version.substring(0, axe.version.lastIndexOf('.'))) + '/' + ruleId + '?application=' + application; } Audit.prototype._constructHelpUrls = function (previous = null) { var version = (axe.version.match(/^[1-9][0-9]*\.[0-9]+/) || ['x.y'])[0]; this.rules.forEach(rule => { if (!this.data.rules[rule.id]) { this.data.rules[rule.id] = {}; } let metaData = this.data.rules[rule.id]; if ( typeof metaData.helpUrl !== 'string' || (previous && metaData.helpUrl === getHelpUrl(previous, rule.id, version)) ) { metaData.helpUrl = getHelpUrl(this, rule.id, version); } }); }; /** * Reset the default rules, checks and meta data */ Audit.prototype.resetRulesAndChecks = function () { 'use strict'; this._init(); };