axe-core
Version:
Accessibility engine for automated Web UI testing
216 lines (186 loc) • 6.12 kB
JavaScript
import metadataFunctionMap from './metadata-function-map';
import CheckResult from './check-result';
import { DqElement, checkHelper, deepMerge } from '../utils';
export function createExecutionContext(spec) {
/*eslint no-eval:0 */
if (typeof spec === 'string') {
if (metadataFunctionMap[spec]) {
return metadataFunctionMap[spec];
}
// execution contexts can only be functions
if (/^\s*function[\s\w]*\(/.test(spec)) {
return new Function('return ' + spec + ';')();
}
throw new ReferenceError(
`Function ID does not exist in the metadata-function-map: ${spec}`
);
}
return spec;
}
/**
* Normalize check options to always be an object.
* @param {Object} options
* @return Object
*/
function normalizeOptions(options = {}) {
if (Array.isArray(options) || typeof options !== 'object') {
options = { value: options };
}
return options;
}
function Check(spec) {
if (spec) {
this.id = spec.id;
this.configure(spec);
}
}
/**
* Unique ID for the check. Checks may be re-used, so there may be additional instances of checks
* with the same ID.
* @type {String}
*/
// Check.prototype.id;
/**
* Free-form options that are passed as the second parameter to the `evaluate`
* @type {Mixed}
*/
// Check.prototype.options;
/**
* The actual code, accepts 2 parameters: node (the node under test), options (see this.options).
* This function is run in the context of a checkHelper, which has the following methods
* - `async()` - if called, the check is considered to be asynchronous; returns a callback function
* - `data()` - free-form data object, associated to the `CheckResult` which is specific to each node
* @type {Function}
*/
// Check.prototype.evaluate;
/**
* Optional. Filter and/or modify checks for all nodes
* @type {Function}
*/
// Check.prototype.after;
/**
* enabled by default, if false, this check will not be included in the rule's evaluation
* @type {Boolean}
*/
Check.prototype.enabled = true;
/**
* Run the check's evaluate function (call `this.evaluate(node, options)`)
* @param {HTMLElement} node The node to test
* @param {Object} options The options that override the defaults and provide additional
* information for the check
* @param {Function} callback Function to fire when check is complete
*/
Check.prototype.run = function run(node, options, context, resolve, reject) {
options = options || {};
const enabled = options.hasOwnProperty('enabled')
? options.enabled
: this.enabled;
const checkOptions = this.getOptions(options.options);
if (enabled) {
const checkResult = new CheckResult(this);
const helper = checkHelper(checkResult, options, resolve, reject);
let result;
try {
result = this.evaluate.call(
helper,
node.actualNode,
checkOptions,
node,
context
);
} catch (e) {
// In the "Audit#run: should run all the rules" test, there is no `node` here. I do
// not know if this is intentional or not, so to be safe, we guard against the
// possible reference error.
if (node && node.actualNode) {
// Save a reference to the node we errored on for futher debugging.
e.errorNode = new DqElement(node).toJSON();
}
reject(e);
return;
}
if (!helper.isAsync) {
checkResult.result = result;
resolve(checkResult);
}
} else {
resolve(null);
}
};
/**
* Run the check's evaluate function (call `this.evaluate(node, options)`) synchronously
* @param {HTMLElement} node The node to test
* @param {Object} options The options that override the defaults and provide additional
* information for the check
*/
Check.prototype.runSync = function runSync(node, options, context) {
options = options || {};
const { enabled = this.enabled } = options;
if (!enabled) {
return null;
}
const checkOptions = this.getOptions(options.options);
const checkResult = new CheckResult(this);
const helper = checkHelper(checkResult, options);
// throw error if a check is run that requires async behavior
helper.async = function async() {
throw new Error('Cannot run async check while in a synchronous run');
};
let result;
try {
result = this.evaluate.call(
helper,
node.actualNode,
checkOptions,
node,
context
);
} catch (e) {
// In the "Audit#run: should run all the rules" test, there is no `node` here. I do
// not know if this is intentional or not, so to be safe, we guard against the
// possible reference error.
if (node && node.actualNode) {
// Save a reference to the node we errored on for futher debugging.
e.errorNode = new DqElement(node).toJSON();
}
throw e;
}
checkResult.result = result;
return checkResult;
};
/**
* Override a check's settings after construction to allow for changing options
* without having to implement the entire check
*
* @param {Object} spec - the specification of the attributes to be changed
*/
Check.prototype.configure = function configure(spec) {
// allow test specs (without evaluate functions) to work as
// internal checks
if (!spec.evaluate || metadataFunctionMap[spec.evaluate]) {
this._internalCheck = true;
}
if (spec.hasOwnProperty('enabled')) {
this.enabled = spec.enabled;
}
if (spec.hasOwnProperty('options')) {
// only normalize options for internal checks
if (this._internalCheck) {
this.options = normalizeOptions(spec.options);
} else {
this.options = spec.options;
}
}
['evaluate', 'after']
.filter(prop => spec.hasOwnProperty(prop))
.forEach(prop => (this[prop] = createExecutionContext(spec[prop])));
};
Check.prototype.getOptions = function getOptions(options) {
// only merge and normalize options for internal checks
if (this._internalCheck) {
return deepMerge(this.options, normalizeOptions(options || {}));
} else {
return options || this.options;
}
};
export default Check;