Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rule,check): add new apis to run a rule synchronously #1467

Merged
merged 7 commits into from
May 10, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions lib/core/base/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,58 @@ Check.prototype.run = function(node, options, context, resolve, reject) {
}
};

/**
* 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(node, options, context) {
/* eslint max-statements: ["error", 17] */
'use strict';
options = options || {};
var enabled = options.hasOwnProperty('enabled')
straker marked this conversation as resolved.
Show resolved Hide resolved
straker marked this conversation as resolved.
Show resolved Hide resolved
? options.enabled
: this.enabled,
checkOptions = options.options || this.options;

if (enabled) {
straker marked this conversation as resolved.
Show resolved Hide resolved
var checkResult = new CheckResult(this);
var checkHelper = axe.utils.checkHelper(checkResult, options);

// throw error if a check is run that requires async behavior
checkHelper.async = function() {
throw new Error('Cannot run async check while in a synchronous run');
};

var result;

try {
result = this.evaluate.call(
checkHelper,
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) {
straker marked this conversation as resolved.
Show resolved Hide resolved
// Save a reference to the node we errored on for futher debugging.
e.errorNode = new DqElement(node.actualNode).toJSON();
}
throw e;
}

checkResult.result = result;
return checkResult;
} else {
return null;
}
};

/**
* Override a check's settings after construction to allow for changing options
* without having to implement the entire check
Expand Down
186 changes: 150 additions & 36 deletions lib/core/base/rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,28 @@ Rule.prototype.runChecks = function(
.catch(reject);
};

/**
* Run a check for a rule synchronously.
*/
Rule.prototype.runChecksSync = function(type, node, options, context) {
'use strict';

const self = this;
let results = [];

this[type].forEach(function(c) {
var check = self._audit.checks[c.id || c];
straker marked this conversation as resolved.
Show resolved Hide resolved
var option = axe.utils.getCheckOption(check, self.id, options);
results.push(check.runSync(node, option, context));
});

results = results.filter(function(check) {
straker marked this conversation as resolved.
Show resolved Hide resolved
return check;
});

return { type: type, results: results };
};

/**
* Runs the Rule's `evaluate` function
* @param {Context} context The resolved Context object
Expand All @@ -144,13 +166,9 @@ Rule.prototype.runChecks = function(
Rule.prototype.run = function(context, options, resolve, reject) {
/*eslint max-statements: ["error",17] */

this._trackPerformance();
const q = axe.utils.queue();
const ruleResult = new RuleResult(this);
const markStart = 'mark_rule_start_' + this.id;
const markEnd = 'mark_rule_end_' + this.id;
const markChecksStart = 'mark_runchecks_start_' + this.id;
const markChecksEnd = 'mark_runchecks_end_' + this.id;

let nodes;

try {
Expand All @@ -165,13 +183,7 @@ Rule.prototype.run = function(context, options, resolve, reject) {
}

if (options.performanceTimer) {
axe.log(
'gather (',
nodes.length,
'):',
axe.utils.performanceTimer.timeElapsed() + 'ms'
);
axe.utils.performanceTimer.mark(markChecksStart);
logGatherPerformance(this, nodes);
}

nodes.forEach(node => {
Expand All @@ -186,22 +198,10 @@ Rule.prototype.run = function(context, options, resolve, reject) {

checkQueue
.then(function(results) {
if (results.length) {
var hasResults = false,
result = {};
results.forEach(function(r) {
var res = r.results.filter(function(result) {
return result;
});
result[r.type] = res;
if (res.length) {
hasResults = true;
}
});
if (hasResults) {
result.node = new axe.utils.DqElement(node.actualNode, options);
ruleResult.nodes.push(result);
}
const result = getResult(results);
if (result) {
result.node = new axe.utils.DqElement(node.actualNode, options);
ruleResult.nodes.push(result);
}
resolveNode();
})
Expand All @@ -214,20 +214,134 @@ Rule.prototype.run = function(context, options, resolve, reject) {
q.defer(resolve => setTimeout(resolve, 0));

if (options.performanceTimer) {
axe.utils.performanceTimer.mark(markChecksEnd);
axe.utils.performanceTimer.mark(markEnd);
axe.utils.performanceTimer.measure(
'runchecks_' + this.id,
markChecksStart,
markChecksEnd
logRulePerformance(this);
}

q.then(() => resolve(ruleResult)).catch(error => reject(error));
};

/**
* Runs the Rule's `evaluate` function synchronously
* @param {Context} context The resolved Context object
* @param {Mixed} options Options specific to this rule
*/
Rule.prototype.runSync = function(context, options) {
/*eslint max-statements: ["error",17] */

options = options || {};
this._trackPerformance();
const ruleResult = new RuleResult(this);
let nodes;

try {
// Matches throws an error when it lacks support for document methods
nodes = this.gather(context).filter(node =>
this.matches(node.actualNode, node, context)
);
} catch (error) {
// Exit the rule execution if matches fails
throw new SupportError({ cause: error, ruleId: this.id });
}

axe.utils.performanceTimer.measure('rule_' + this.id, markStart, markEnd);
if (options.performanceTimer) {
logGatherPerformance(this, nodes);
}

q.then(() => resolve(ruleResult)).catch(error => reject(error));
nodes.forEach(node => {
let results = [];
['any', 'all', 'none'].forEach(type => {
results.push(this.runChecksSync(type, node, options, context));
});

const result = getResult(results);
if (result) {
result.node = new axe.utils.DqElement(node.actualNode, options);
ruleResult.nodes.push(result);
}
});

if (options.performanceTimer) {
logRulePerformance(this);
}

return ruleResult;
};

/**
* Add performance tracking properties to the rule
* @private
*/
Rule.prototype._trackPerformance = function() {
straker marked this conversation as resolved.
Show resolved Hide resolved
this._markStart = 'mark_rule_start_' + this.id;
this._markEnd = 'mark_rule_end_' + this.id;
this._markChecksStart = 'mark_runchecks_start_' + this.id;
this._markChecksEnd = 'mark_runchecks_end_' + this.id;
};

/**
* Log performance of rule.gather
* @param {Rule} rule The rule to log
* @param {Array} nodes Result of rule.gather
*/
function logGatherPerformance(rule, nodes) {
straker marked this conversation as resolved.
Show resolved Hide resolved
axe.log(
'gather (',
nodes.length,
'):',
axe.utils.performanceTimer.timeElapsed() + 'ms'
);
axe.utils.performanceTimer.mark(rule._markChecksStart);
}

/**
* Log performance of the rule
* @param {Rule} rule The rule to log
*/
function logRulePerformance(rule) {
axe.utils.performanceTimer.mark(rule._markChecksEnd);
axe.utils.performanceTimer.mark(rule._markEnd);
axe.utils.performanceTimer.measure(
'runchecks_' + rule.id,
rule._markChecksStart,
rule._markChecksEnd
);

axe.utils.performanceTimer.measure(
'rule_' + rule.id,
rule._markStart,
rule._markEnd
);
}

/**
* Process the results of each check and return the result if a check
* has a result
* @private
* @param {Array} results Array of each check result
* @returns {Object|null}
*/
function getResult(results) {
if (results.length) {
let hasResults = false,
result = {};
results.forEach(function(r) {
const res = r.results.filter(function(result) {
return result;
});
result[r.type] = res;
if (res.length) {
hasResults = true;
}
});

if (hasResults) {
return result;
}

return null;
}
}

/**
* Iterates the rule's Checks looking for ones that have an after function
* @private
Expand Down
Loading