From 74a0f0104e04f0b3c2bc9b9378014a42c95cd063 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Fri, 22 Mar 2019 08:27:51 -0600 Subject: [PATCH 1/6] feat: add api to support linting --- lib/core/public/get-rule.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 lib/core/public/get-rule.js diff --git a/lib/core/public/get-rule.js b/lib/core/public/get-rule.js new file mode 100644 index 0000000000..4e2b84367a --- /dev/null +++ b/lib/core/public/get-rule.js @@ -0,0 +1,10 @@ +/** + * Searches and returns rule that matches the ruleId + * @param {String} ruleId Id of the rule + * @return {Rule} rule + */ +axe.getRule = function(ruleId) { + 'use strict'; + + return axe._audit.rules.find(rule => rule.id === ruleId); +}; From d722001614b77868b497adf834bab8fb4040987c Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Mon, 1 Apr 2019 16:06:55 -0600 Subject: [PATCH 2/6] finalize api --- lib/core/base/check.js | 52 +++ lib/core/base/rule.js | 186 +++++++-- lib/core/public/get-rule.js | 10 - lib/core/public/run-virtual-rule.js | 33 ++ lib/core/public/virtual-node.js | 91 +++++ test/core/base/check.js | 118 ++++++ test/core/base/rule.js | 578 +++++++++++++++++++++++++++ test/core/public/run-virtual-rule.js | 147 +++++++ test/core/public/virtual-node.js | 192 +++++++++ 9 files changed, 1361 insertions(+), 46 deletions(-) delete mode 100644 lib/core/public/get-rule.js create mode 100644 lib/core/public/run-virtual-rule.js create mode 100644 lib/core/public/virtual-node.js create mode 100644 test/core/public/run-virtual-rule.js create mode 100644 test/core/public/virtual-node.js diff --git a/lib/core/base/check.js b/lib/core/base/check.js index 306dfadff2..0868cf772b 100644 --- a/lib/core/base/check.js +++ b/lib/core/base/check.js @@ -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') + ? options.enabled + : this.enabled, + checkOptions = options.options || this.options; + + if (enabled) { + 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) { + // 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 diff --git a/lib/core/base/rule.js b/lib/core/base/rule.js index db6bc6b88b..d387646296 100644 --- a/lib/core/base/rule.js +++ b/lib/core/base/rule.js @@ -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]; + var option = axe.utils.getCheckOption(check, self.id, options); + results.push(check.runSync(node, option, context)); + }); + + results = results.filter(function(check) { + return check; + }); + + return { type: type, results: results }; +}; + /** * Runs the Rule's `evaluate` function * @param {Context} context The resolved Context object @@ -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 { @@ -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 => { @@ -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(); }) @@ -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() { + 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) { + 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 diff --git a/lib/core/public/get-rule.js b/lib/core/public/get-rule.js deleted file mode 100644 index 4e2b84367a..0000000000 --- a/lib/core/public/get-rule.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Searches and returns rule that matches the ruleId - * @param {String} ruleId Id of the rule - * @return {Rule} rule - */ -axe.getRule = function(ruleId) { - 'use strict'; - - return axe._audit.rules.find(rule => rule.id === ruleId); -}; diff --git a/lib/core/public/run-virtual-rule.js b/lib/core/public/run-virtual-rule.js new file mode 100644 index 0000000000..3b958c12e7 --- /dev/null +++ b/lib/core/public/run-virtual-rule.js @@ -0,0 +1,33 @@ +/** + * Run a rule in a non-browser environment + * @param {String} ruleId Id of the rule + * @param {Node} node The virtual node to run the rule against + * @param {Object} options (optional) Set of options passed into rules or checks + * @returns {Object} axe results for the rule run + */ +axe.runVirtualRule = function(ruleId, node, options) { + 'use strict'; + let rule = axe._audit.rules.find(rule => rule.id === ruleId); + + if (!rule) { + return null; + } + + // rule.prototype.gather will try to call axe.utils.isHidden if the + // rule does not exclude hidden elements. This function tries to call + // window.getComputedStyle, so we can avoid this call by forcing the + // rule to not exclude hidden elements + rule.excludeHidden = false; + + const context = { + include: [ + { + actualNode: node + } + ] + }; + + const results = rule.runSync(context, options); + axe.utils.publishMetaData(results); + return results; +}; diff --git a/lib/core/public/virtual-node.js b/lib/core/public/virtual-node.js new file mode 100644 index 0000000000..cf472fdf84 --- /dev/null +++ b/lib/core/public/virtual-node.js @@ -0,0 +1,91 @@ +/** + * A virtual attribute used for virtual nodes. + * @param {String} name Attribute name + * @param {String} value Attribute value + */ +class VirtualAttribute { + constructor(name, value) { + this.name = name; + this.value = value; + } +} + +/** + * A virtual node implementation of Element and Node apis that can + * be used by axe in non-browser environments. + * @param {String} type Name of the node + * @param {Object} attributes Attributes of the node + */ +axe.VirtualNode = class VirtualNode { + constructor(type, attributes = {}) { + this.nodeName = type.toUpperCase(); + this.nodeType = 1; + + this.attributes = { + length: Object.keys(attributes).length + }; + Object.keys(attributes).forEach((name, index) => { + const attribute = new VirtualAttribute(name, attributes[name]); + this.attributes[name] = attribute; + this.attributes[index] = attribute; + }); + + this.tabIndex = this.hasAttribute('tabindex') + ? this.getAttribute('tabindex') + : -1; + } + + getAttribute(attributeName) { + if (typeof attributeName !== 'string') { + return null; + } + + let attribute = this.attributes[attributeName]; + + if (!attribute) { + return null; + } + + return attribute.value; + } + + hasAttribute(attributeName) { + if (typeof attributeName !== 'string') { + return false; + } + + return !!this.attributes[attributeName]; + } + + hasAttributes() { + return this.attributes.length > 0; + } + + // used by axe.utils.select to find nodes that match the rules + // selector. We can reroute the function to call + // axe.utils.querySelectorAll, which doesn’t rely on any browser apis + // to run. However, it gets passed an Axe virtual node, so we need to + // mock that as well + matches(selector) { + return ( + axe.utils.querySelectorAll({ actualNode: this }, selector).length > 0 + ); + } + + // used by axe.utils.contains to check if the node is in context + contains(node) { + return this === node; + } + + // used by axe.utils.getNodeAttributes when the attributes object is + // not of type NamedNodeMap. Since we're a virtual node and won't + // have the problem of being clobbered, we can just return the node + cloneNode() { + return this; + } + + // used by DqElement to get the source. Must exist so the + // XMLSerializer doesn’t get called with the node (which would throw + // an error) + outerHTML() {} +}; diff --git a/test/core/base/check.js b/test/core/base/check.js index 0ae8b93944..1c01b153eb 100644 --- a/test/core/base/check.js +++ b/test/core/base/check.js @@ -274,6 +274,124 @@ describe('Check', function() { }); }); }); + + describe('runSync', function() { + it('should accept 3 parameters', function() { + assert.lengthOf(new Check({}).runSync, 3); + }); + + it('should pass the node through', function() { + new Check({ + evaluate: function(node) { + assert.equal(node, fixture); + } + }).runSync(axe.utils.getFlattenedTree(fixture)[0], {}, {}); + }); + + it('should pass the options through', function() { + var expected = { monkeys: 'bananas' }; + + new Check({ + options: expected, + evaluate: function(node, options) { + assert.deepEqual(options, expected); + } + }).runSync(fixture, {}, {}); + }); + + it('should pass the options through modified by the ones passed into the call', function() { + var configured = { monkeys: 'bananas' }, + expected = { monkeys: 'bananas', dogs: 'cats' }; + + new Check({ + options: configured, + evaluate: function(node, options) { + assert.deepEqual(options, expected); + } + }).runSync(fixture, { options: expected }, {}); + }); + + it('should pass the context through to check evaluate call', function() { + var configured = { + cssom: 'yay', + source: 'this is page source', + aom: undefined + }; + new Check({ + options: configured, + evaluate: function(node, options, virtualNode, context) { + assert.property(context, 'cssom'); + assert.deepEqual(context, configured); + } + }).runSync(fixture, {}, configured); + }); + + it('should throw error for asynchronous checks', function() { + var data = { monkeys: 'bananas' }; + + try { + new Check({ + evaluate: function() { + var ready = this.async(); + setTimeout(function() { + ready(data); + }, 10); + } + }).runSync(fixture, {}, {}); + } catch (err) { + assert.instanceOf(err, Error); + assert.equal( + err.message, + 'Cannot run async check while in a synchronous run' + ); + } + }); + + it('should pass `null` as the parameter if not enabled', function() { + let data = new Check({ + evaluate: function() {}, + enabled: false + }).runSync(fixture, {}, {}); + + assert.isNull(data); + }); + + it('should pass `null` as the parameter if options disable', function() { + let data = new Check({ + evaluate: function() {} + }).runSync( + fixture, + { + enabled: false + }, + {} + ); + assert.isNull(data); + }); + + it('passes a result to the resolve argument', function() { + let data = new Check({ + evaluate: function() { + return true; + } + }).runSync(fixture, {}, {}); + assert.instanceOf(data, CheckResult); + assert.isTrue(data.result); + }); + + it('should throw errors', function() { + try { + new Check({ + evaluate: function() { + throw new Error('Grenade!'); + } + }).runSync(fixture, {}, {}); + } catch (err) { + assert.instanceOf(err, Error); + assert.equal(err.message, 'Grenade!'); + } + }); + }); }); describe('spec object', function() { diff --git a/test/core/base/rule.js b/test/core/base/rule.js index 1234c1548c..86d8ff673e 100644 --- a/test/core/base/rule.js +++ b/test/core/base/rule.js @@ -149,6 +149,7 @@ describe('Rule', function() { ); }); }); + describe('run', function() { it('should be a function', function() { assert.isFunction(Rule.prototype.run); @@ -755,6 +756,583 @@ describe('Rule', function() { }); }); + describe('runSync', function() { + it('should be a function', function() { + assert.isFunction(Rule.prototype.runSync); + }); + + it('should run #matches', function() { + var div = document.createElement('div'); + fixture.appendChild(div); + var success = false, + rule = new Rule({ + matches: function(node) { + assert.equal(node, div); + success = true; + return []; + } + }); + + try { + rule.runSync( + { + include: [axe.utils.getFlattenedTree(div)[0]] + }, + {} + ); + assert.isTrue(success); + } catch (err) { + isNotCalled(err); + } + }); + + it('should pass a virtualNode to #matches', function() { + var div = document.createElement('div'); + fixture.appendChild(div); + var success = false, + rule = new Rule({ + matches: function(node, virtualNode) { + assert.equal(virtualNode.actualNode, div); + success = true; + return []; + } + }); + + try { + rule.runSync( + { + include: [axe.utils.getFlattenedTree(div)[0]] + }, + {} + ); + assert.isTrue(success); + } catch (err) { + isNotCalled(err); + } + }); + + it('should pass a context to #matches', function() { + var div = document.createElement('div'); + fixture.appendChild(div); + var success = false, + rule = new Rule({ + matches: function(node, virtualNode, context) { + assert.isDefined(context); + assert.hasAnyKeys(context, ['cssom', 'include', 'exclude']); + assert.lengthOf(context.include, 1); + success = true; + return []; + } + }); + + try { + rule.runSync( + { + include: [axe.utils.getFlattenedTree(div)[0]] + }, + {} + ); + assert.isTrue(success); + } catch (err) { + isNotCalled(err); + } + }); + + it('should handle an error in #matches', function() { + var div = document.createElement('div'); + div.setAttribute('style', '#fff'); + fixture.appendChild(div); + var success = false, + rule = new Rule({ + matches: function() { + throw new Error('this is an error'); + } + }); + + try { + rule.runSync( + { + include: [axe.utils.getFlattenedTree(div)[0]] + }, + {} + ); + isNotCalled(); + } catch (err) { + assert.isFalse(success); + } + }); + + it('should execute Check#runSync on its child checks - any', function() { + fixture.innerHTML = 'Hi'; + var success = false; + var rule = new Rule( + { + any: ['cats'] + }, + { + checks: { + cats: { + runSync: function() { + success = true; + } + } + } + } + ); + + try { + rule.runSync( + { + include: [axe.utils.getFlattenedTree(fixture)[0]] + }, + {} + ); + assert.isTrue(success); + } catch (err) { + isNotCalled(err); + } + }); + + it('should execute Check#runSync on its child checks - all', function() { + fixture.innerHTML = 'Hi'; + var success = false; + var rule = new Rule( + { + all: ['cats'] + }, + { + checks: { + cats: { + runSync: function() { + success = true; + } + } + } + } + ); + + try { + rule.runSync( + { + include: [axe.utils.getFlattenedTree(fixture)[0]] + }, + {} + ); + assert.isTrue(success); + } catch (err) { + isNotCalled(err); + } + }); + + it('should execute Check#run on its child checks - none', function() { + fixture.innerHTML = 'Hi'; + var success = false; + var rule = new Rule( + { + none: ['cats'] + }, + { + checks: { + cats: { + runSync: function() { + success = true; + } + } + } + }, + isNotCalled + ); + + try { + rule.runSync( + { + include: [axe.utils.getFlattenedTree(fixture)[0]] + }, + {} + ); + assert.isTrue(success); + } catch (err) { + isNotCalled(err); + } + }); + + it('should pass the matching option to check.runSync', function() { + fixture.innerHTML = 'Hi'; + var options = { + checks: { + cats: { + enabled: 'bananas', + options: 'minkeys' + } + } + }; + var rule = new Rule( + { + none: ['cats'] + }, + { + checks: { + cats: { + id: 'cats', + runSync: function(node, options) { + assert.equal(options.enabled, 'bananas'); + assert.equal(options.options, 'minkeys'); + } + } + } + } + ); + + try { + rule.runSync( + { + include: [axe.utils.getFlattenedTree(document)[0]] + }, + options + ); + } catch (err) { + isNotCalled(err); + } + }); + + it('should pass the matching option to check.runSync defined on the rule over global', function() { + fixture.innerHTML = 'Hi'; + var options = { + rules: { + cats: { + checks: { + cats: { + enabled: 'apples', + options: 'apes' + } + } + } + }, + checks: { + cats: { + enabled: 'bananas', + options: 'minkeys' + } + } + }; + + var rule = new Rule( + { + id: 'cats', + any: [ + { + id: 'cats' + } + ] + }, + { + checks: { + cats: { + id: 'cats', + runSync: function(node, options) { + assert.equal(options.enabled, 'apples'); + assert.equal(options.options, 'apes'); + } + } + } + } + ); + + try { + rule.runSync( + { + include: [axe.utils.getFlattenedTree(document)[0]] + }, + options + ); + } catch (err) { + isNotCalled(err); + } + }); + + it('should filter out null results', function() { + var rule = new Rule( + { + selector: '#fixture', + any: ['cats'] + }, + { + checks: { + cats: { + id: 'cats', + runSync: function() {} + } + } + } + ); + + try { + let r = rule.runSync( + { + include: [axe.utils.getFlattenedTree(document)[0]] + }, + {} + ); + assert.lengthOf(r.nodes, 0); + } catch (err) { + isNotCalled(err); + } + }); + + describe('DqElement', function() { + var origDqElement; + var isDqElementCalled; + + beforeEach(function() { + isDqElementCalled = false; + origDqElement = axe.utils.DqElement; + axe.utils.DqElement = function() { + isDqElementCalled = true; + }; + fixture.innerHTML = 'Hi'; + }); + + afterEach(function() { + axe.utils.DqElement = origDqElement; + }); + + it('is created for matching nodes', function() { + var rule = new Rule( + { + all: ['cats'] + }, + { + checks: { + cats: new Check({ + id: 'cats', + enabled: true, + evaluate: function() { + return true; + }, + matches: function() { + return true; + } + }) + } + } + ); + + try { + rule.runSync( + { + include: [axe.utils.getFlattenedTree(fixture)[0]] + }, + {} + ); + assert.isTrue(isDqElementCalled); + } catch (err) { + isNotCalled(err); + } + }); + + it('is not created for disabled checks', function() { + var rule = new Rule( + { + all: ['cats'] + }, + { + checks: { + cats: new Check({ + id: 'cats', + enabled: false, + evaluate: function() {}, + matches: function() { + return true; + } + }) + } + } + ); + + try { + rule.runSync( + { + include: [axe.utils.getFlattenedTree(fixture)[0]] + }, + {} + ); + assert.isFalse(isDqElementCalled); + } catch (err) { + isNotCalled(err); + } + }); + + it('is created for matching nodes', function() { + var rule = new Rule( + { + all: ['cats'] + }, + { + checks: { + cats: new Check({ + id: 'cats', + enabled: true, + evaluate: function() { + return true; + } + }) + } + } + ); + + try { + rule.runSync( + { + include: [axe.utils.getFlattenedTree(fixture)[0]] + }, + {} + ); + assert.isTrue(isDqElementCalled); + } catch (err) { + isNotCalled(err); + } + }); + + it('is not created for disabled checks', function() { + var rule = new Rule( + { + all: ['cats'] + }, + { + checks: { + cats: new Check({ + id: 'cats', + enabled: false, + evaluate: function() {} + }) + } + } + ); + rule.runSync( + { + include: [axe.utils.getFlattenedTree(fixture)[0]] + }, + {}, + function() { + assert.isFalse(isDqElementCalled); + }, + isNotCalled + ); + }); + }); + + it('should pass thrown errors to the reject param', function() { + fixture.innerHTML = 'Hi'; + var rule = new Rule( + { + none: ['cats'] + }, + { + checks: { + cats: { + runSync: function() { + throw new Error('Holy hand grenade'); + } + } + } + } + ); + + try { + rule.runSync( + { + include: [axe.utils.getFlattenedTree(fixture)[0]] + }, + {} + ); + isNotCalled(); + } catch (err) { + assert.equal(err.message, 'Holy hand grenade'); + } + }); + + describe('NODE rule', function() { + it('should create a RuleResult', function() { + var orig = window.RuleResult; + var success = false; + window.RuleResult = function(r) { + this.nodes = []; + assert.equal(rule, r); + success = true; + }; + + var rule = new Rule( + { + any: [ + { + evaluate: function() {}, + id: 'cats' + } + ] + }, + { + checks: { + cats: { + runSync: function() {} + } + } + } + ); + + try { + rule.runSync( + { + include: [axe.utils.getFlattenedTree(document)[0]] + }, + {} + ); + assert.isTrue(success); + } catch (err) { + isNotCalled(err); + } + + window.RuleResult = orig; + }); + + it('should execute rule callback', function() { + var success = false; + + var rule = new Rule( + { + any: [ + { + evaluate: function() {}, + id: 'cats' + } + ] + }, + { + checks: { + cats: { + runSync: function() { + success = true; + } + } + } + } + ); + + try { + rule.runSync( + { + include: [axe.utils.getFlattenedTree(document)[0]] + }, + {} + ); + } catch (err) { + isNotCalled(err); + } + + assert.isTrue(success); + }); + }); + }); + describe('after', function() { it('should execute Check#after with options', function() { var success = false; diff --git a/test/core/public/run-virtual-rule.js b/test/core/public/run-virtual-rule.js new file mode 100644 index 0000000000..4d2c5c1d47 --- /dev/null +++ b/test/core/public/run-virtual-rule.js @@ -0,0 +1,147 @@ +describe('axe.runVirtualRule', function() { + 'use strict'; + const audit = axe._audit; + + beforeEach(function() { + axe._audit = { + data: {} + }; + }); + + afterEach(function() { + axe._audit = audit; + }); + + it('should return null if the rule does not exist', function() { + axe._audit.rules = []; + assert.equal(axe.runVirtualRule('aria-roles'), null); + }); + + it('should modify the rule to not excludeHidden', function() { + axe._audit.rules = [ + { + id: 'aria-roles', + excludeHidden: true, + runSync: function() { + return { + id: 'aria-roles', + nodes: [] + }; + } + } + ]; + + axe.runVirtualRule('aria-roles'); + assert.isFalse(axe._audit.rules[0].excludeHidden); + }); + + it('should call rule.runSync', function() { + let success = false; + axe._audit.rules = [ + { + id: 'aria-roles', + runSync: function() { + success = true; + return { + id: 'aria-roles', + nodes: [] + }; + } + } + ]; + + axe.runVirtualRule('aria-roles'); + assert.isTrue(success); + }); + + it('should pass a virtual context to rule.runSync', function() { + axe._audit.rules = [ + { + id: 'aria-roles', + runSync: function(context) { + assert.equal(typeof context, 'object'); + assert.isTrue(Array.isArray(context.include)); + assert.equal(typeof context.include[0], 'object'); + + return { + id: 'aria-roles', + nodes: [] + }; + } + } + ]; + + axe.runVirtualRule('aria-roles'); + }); + + it('should pass the virtualNode as the actualNode of the context', function() { + const node = {}; + axe._audit.rules = [ + { + id: 'aria-roles', + runSync: function(context) { + assert.equal(context.include[0].actualNode, node); + + return { + id: 'aria-roles', + nodes: [] + }; + } + } + ]; + + axe.runVirtualRule('aria-roles', node); + }); + + it('should pass through options to rule.runSync', function() { + axe._audit.rules = [ + { + id: 'aria-roles', + runSync: function(context, options) { + assert.equal(options.foo, 'bar'); + + return { + id: 'aria-roles', + nodes: [] + }; + } + } + ]; + + axe.runVirtualRule('aria-roles', null, { foo: 'bar' }); + }); + + it('should pass the results of rule.runSync to axe.utils.publishMetaData', function() { + const publishMetaData = axe.utils.publishMetaData; + axe.utils.publishMetaData = function(results) { + assert.isTrue(results); + }; + axe._audit.rules = [ + { + id: 'aria-roles', + runSync: function() { + return true; + } + } + ]; + + axe.runVirtualRule('aria-roles'); + axe.utils.publishMetaData = publishMetaData; + }); + + it('should return the results of rule.runSync', function() { + const publishMetaData = axe.utils.publishMetaData; + axe.utils.publishMetaData = function() {}; + axe._audit.rules = [ + { + id: 'aria-roles', + runSync: function() { + return true; + } + } + ]; + + assert.isTrue(axe.runVirtualRule('aria-roles')); + axe.utils.publishMetaData = publishMetaData; + }); +}); diff --git a/test/core/public/virtual-node.js b/test/core/public/virtual-node.js new file mode 100644 index 0000000000..91aed1caed --- /dev/null +++ b/test/core/public/virtual-node.js @@ -0,0 +1,192 @@ +describe('axe.VirtualNode', function() { + 'use strict'; + + it('should return an object', function() { + const node = new axe.VirtualNode('div'); + assert.equal(typeof node, 'object'); + }); + + it('should have a nodeName', function() { + const node = new axe.VirtualNode('div'); + assert.equal(node.nodeName, 'DIV'); + }); + + it('should have a nodeType', function() { + const node = new axe.VirtualNode('div'); + assert.equal(node.nodeType, 1); + }); + + it('should have attributes', function() { + const node = new axe.VirtualNode('div'); + assert.equal(typeof node.attributes, 'object'); + assert.equal(node.attributes.length, 0); + }); + + it('should allow passing an attribute', function() { + const node = new axe.VirtualNode('div', { id: 'myDiv' }); + assert.equal(node.attributes.length, 1); + let id = node.attributes.id; + assert.equal(node.attributes[0], id); + assert.equal(typeof id, 'object'); + assert.equal(id.name, 'id'); + assert.equal(id.value, 'myDiv'); + }); + + it('should allow passing multiple attributes', function() { + const node = new axe.VirtualNode('button', { + id: 'myButton', + type: 'button', + 'aria-label': 'My Button', + class: 'btn btn-primary' + }); + assert.equal(node.attributes.length, 4); + + for (let i = 0; i < node.attributes.length; i++) { + let attribute = node.attributes[i]; + assert.equal(attribute, node.attributes[attribute.name]); + } + }); + + it('should have a tabindex', function() { + const node = new axe.VirtualNode('div'); + assert.equal(node.tabIndex, -1); + }); + + it('should use the tabindex attribute if it exists', function() { + const node = new axe.VirtualNode('div', { tabindex: 10 }); + assert.equal(node.tabIndex, 10); + }); + + describe('getAttribute', function() { + it('should return the attribute value', function() { + const node = new axe.VirtualNode('div', { id: 'myDiv' }); + assert.equal(node.getAttribute('id'), 'myDiv'); + }); + + it('should return null if the attribute does not exist', function() { + const node = new axe.VirtualNode('div', { id: 'myDiv' }); + assert.equal(node.getAttribute('tabindex'), null); + }); + + it('should return null if passed bad data', function() { + const node = new axe.VirtualNode('div', { id: 'myDiv' }); + assert.equal(node.getAttribute(0), null); + assert.equal(node.getAttribute(null), null); + assert.equal(node.getAttribute([]), null); + assert.equal(node.getAttribute({}), null); + assert.equal(node.getAttribute(true), null); + }); + }); + + describe('hasAttribute', function() { + it('should return true if the attribute exists', function() { + const node = new axe.VirtualNode('div', { id: 'myDiv' }); + assert.isTrue(node.hasAttribute('id')); + }); + + it('should return false if the attribute does not exist', function() { + const node = new axe.VirtualNode('div'); + assert.isFalse(node.hasAttribute('tabindex')); + }); + + it('should return false if passed bad data', function() { + const node = new axe.VirtualNode('div', { id: 'myDiv' }); + assert.isFalse(node.hasAttribute(null)); + assert.isFalse(node.hasAttribute([])); + assert.isFalse(node.hasAttribute({})); + assert.isFalse(node.hasAttribute(true)); + assert.isFalse(node.hasAttribute(0)); + }); + }); + + describe('hasAttributes', function() { + it('should return true if the node has at least one attributes', function() { + const node = new axe.VirtualNode('div', { id: 'myDiv' }); + assert.isTrue(node.hasAttributes()); + }); + + it('should return false if the node has no attributes', function() { + const node = new axe.VirtualNode('div'); + assert.isFalse(node.hasAttributes()); + }); + }); + + describe('hasAttributes', function() { + it('should return true if the node has at least one attributes', function() { + const node = new axe.VirtualNode('div', { id: 'myDiv' }); + assert.isTrue(node.hasAttributes()); + }); + + it('should return false if the node has no attributes', function() { + const node = new axe.VirtualNode('div'); + assert.isFalse(node.hasAttributes()); + }); + }); + + describe('matches', function() { + let qsa = axe.utils.querySelectorAll; + + afterEach(function() { + axe.utils.querySelectorAll = qsa; + }); + + it('should call axe.utils.querySelectorAll', function() { + const node = new axe.VirtualNode('div', { id: 'myDiv' }); + let success = false; + axe.utils.querySelectorAll = function() { + success = true; + return []; + }; + node.matches('div'); + assert.isTrue(success); + }); + + it('should pass an axe virtual node with the node as the actualNode', function() { + const node = new axe.VirtualNode('div', { id: 'myDiv' }); + axe.utils.querySelectorAll = function(virtualNode) { + assert.equal(virtualNode.actualNode, node); + return []; + }; + node.matches('div'); + }); + + it('should pass the selector', function() { + const node = new axe.VirtualNode('div', { id: 'myDiv' }); + axe.utils.querySelectorAll = function(virtualNode, selector) { + assert.equal(selector, '[id="myDiv"]'); + return []; + }; + node.matches('[id="myDiv"]'); + }); + + it('should return a boolean', function() { + const node = new axe.VirtualNode('div', { id: 'myDiv' }); + axe.utils.querySelectorAll = function() { + return [1]; + }; + let result = node.matches('[id="myDiv"]'); + assert.isTrue(result); + }); + }); + + describe('contains', function() { + it('should return true if the node is equal', function() { + const node = new axe.VirtualNode('div', { id: 'myDiv' }); + assert.isTrue(node.contains(node)); + }); + }); + + describe('cloneNode', function() { + it('should return itself', function() { + const node = new axe.VirtualNode('div', { id: 'myDiv' }); + assert.equal(node, node.cloneNode()); + }); + }); + + describe('outerHTML', function() { + it('should be a function', function() { + const node = new axe.VirtualNode('div', { id: 'myDiv' }); + assert.equal(typeof node.outerHTML, 'function'); + }); + }); +}); From 4f94f26a779f53dc115636a056797d74e6c8336c Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Tue, 2 Apr 2019 13:40:41 -0600 Subject: [PATCH 3/6] dont use let or const in tests --- test/core/base/check.js | 6 +-- test/core/base/rule.js | 2 +- test/core/public/run-virtual-rule.js | 10 ++--- test/core/public/virtual-node.js | 62 ++++++++++++++-------------- 4 files changed, 40 insertions(+), 40 deletions(-) diff --git a/test/core/base/check.js b/test/core/base/check.js index 1c01b153eb..a19334646d 100644 --- a/test/core/base/check.js +++ b/test/core/base/check.js @@ -348,7 +348,7 @@ describe('Check', function() { }); it('should pass `null` as the parameter if not enabled', function() { - let data = new Check({ + var data = new Check({ evaluate: function() {}, enabled: false }).runSync(fixture, {}, {}); @@ -357,7 +357,7 @@ describe('Check', function() { }); it('should pass `null` as the parameter if options disable', function() { - let data = new Check({ + var data = new Check({ evaluate: function() {} }).runSync( fixture, @@ -370,7 +370,7 @@ describe('Check', function() { }); it('passes a result to the resolve argument', function() { - let data = new Check({ + var data = new Check({ evaluate: function() { return true; } diff --git a/test/core/base/rule.js b/test/core/base/rule.js index 86d8ff673e..ca297bf15d 100644 --- a/test/core/base/rule.js +++ b/test/core/base/rule.js @@ -1067,7 +1067,7 @@ describe('Rule', function() { ); try { - let r = rule.runSync( + var r = rule.runSync( { include: [axe.utils.getFlattenedTree(document)[0]] }, diff --git a/test/core/public/run-virtual-rule.js b/test/core/public/run-virtual-rule.js index 4d2c5c1d47..f6806d1089 100644 --- a/test/core/public/run-virtual-rule.js +++ b/test/core/public/run-virtual-rule.js @@ -1,6 +1,6 @@ describe('axe.runVirtualRule', function() { 'use strict'; - const audit = axe._audit; + var audit = axe._audit; beforeEach(function() { axe._audit = { @@ -36,7 +36,7 @@ describe('axe.runVirtualRule', function() { }); it('should call rule.runSync', function() { - let success = false; + var success = false; axe._audit.rules = [ { id: 'aria-roles', @@ -75,7 +75,7 @@ describe('axe.runVirtualRule', function() { }); it('should pass the virtualNode as the actualNode of the context', function() { - const node = {}; + var node = {}; axe._audit.rules = [ { id: 'aria-roles', @@ -112,7 +112,7 @@ describe('axe.runVirtualRule', function() { }); it('should pass the results of rule.runSync to axe.utils.publishMetaData', function() { - const publishMetaData = axe.utils.publishMetaData; + var publishMetaData = axe.utils.publishMetaData; axe.utils.publishMetaData = function(results) { assert.isTrue(results); }; @@ -130,7 +130,7 @@ describe('axe.runVirtualRule', function() { }); it('should return the results of rule.runSync', function() { - const publishMetaData = axe.utils.publishMetaData; + var publishMetaData = axe.utils.publishMetaData; axe.utils.publishMetaData = function() {}; axe._audit.rules = [ { diff --git a/test/core/public/virtual-node.js b/test/core/public/virtual-node.js index 91aed1caed..0add55d05e 100644 --- a/test/core/public/virtual-node.js +++ b/test/core/public/virtual-node.js @@ -2,30 +2,30 @@ describe('axe.VirtualNode', function() { 'use strict'; it('should return an object', function() { - const node = new axe.VirtualNode('div'); + var node = new axe.VirtualNode('div'); assert.equal(typeof node, 'object'); }); it('should have a nodeName', function() { - const node = new axe.VirtualNode('div'); + var node = new axe.VirtualNode('div'); assert.equal(node.nodeName, 'DIV'); }); it('should have a nodeType', function() { - const node = new axe.VirtualNode('div'); + var node = new axe.VirtualNode('div'); assert.equal(node.nodeType, 1); }); it('should have attributes', function() { - const node = new axe.VirtualNode('div'); + var node = new axe.VirtualNode('div'); assert.equal(typeof node.attributes, 'object'); assert.equal(node.attributes.length, 0); }); it('should allow passing an attribute', function() { - const node = new axe.VirtualNode('div', { id: 'myDiv' }); + var node = new axe.VirtualNode('div', { id: 'myDiv' }); assert.equal(node.attributes.length, 1); - let id = node.attributes.id; + var id = node.attributes.id; assert.equal(node.attributes[0], id); assert.equal(typeof id, 'object'); assert.equal(id.name, 'id'); @@ -33,7 +33,7 @@ describe('axe.VirtualNode', function() { }); it('should allow passing multiple attributes', function() { - const node = new axe.VirtualNode('button', { + var node = new axe.VirtualNode('button', { id: 'myButton', type: 'button', 'aria-label': 'My Button', @@ -41,35 +41,35 @@ describe('axe.VirtualNode', function() { }); assert.equal(node.attributes.length, 4); - for (let i = 0; i < node.attributes.length; i++) { - let attribute = node.attributes[i]; + for (var i = 0; i < node.attributes.length; i++) { + var attribute = node.attributes[i]; assert.equal(attribute, node.attributes[attribute.name]); } }); it('should have a tabindex', function() { - const node = new axe.VirtualNode('div'); + var node = new axe.VirtualNode('div'); assert.equal(node.tabIndex, -1); }); it('should use the tabindex attribute if it exists', function() { - const node = new axe.VirtualNode('div', { tabindex: 10 }); + var node = new axe.VirtualNode('div', { tabindex: 10 }); assert.equal(node.tabIndex, 10); }); describe('getAttribute', function() { it('should return the attribute value', function() { - const node = new axe.VirtualNode('div', { id: 'myDiv' }); + var node = new axe.VirtualNode('div', { id: 'myDiv' }); assert.equal(node.getAttribute('id'), 'myDiv'); }); it('should return null if the attribute does not exist', function() { - const node = new axe.VirtualNode('div', { id: 'myDiv' }); + var node = new axe.VirtualNode('div', { id: 'myDiv' }); assert.equal(node.getAttribute('tabindex'), null); }); it('should return null if passed bad data', function() { - const node = new axe.VirtualNode('div', { id: 'myDiv' }); + var node = new axe.VirtualNode('div', { id: 'myDiv' }); assert.equal(node.getAttribute(0), null); assert.equal(node.getAttribute(null), null); assert.equal(node.getAttribute([]), null); @@ -80,17 +80,17 @@ describe('axe.VirtualNode', function() { describe('hasAttribute', function() { it('should return true if the attribute exists', function() { - const node = new axe.VirtualNode('div', { id: 'myDiv' }); + var node = new axe.VirtualNode('div', { id: 'myDiv' }); assert.isTrue(node.hasAttribute('id')); }); it('should return false if the attribute does not exist', function() { - const node = new axe.VirtualNode('div'); + var node = new axe.VirtualNode('div'); assert.isFalse(node.hasAttribute('tabindex')); }); it('should return false if passed bad data', function() { - const node = new axe.VirtualNode('div', { id: 'myDiv' }); + var node = new axe.VirtualNode('div', { id: 'myDiv' }); assert.isFalse(node.hasAttribute(null)); assert.isFalse(node.hasAttribute([])); assert.isFalse(node.hasAttribute({})); @@ -101,38 +101,38 @@ describe('axe.VirtualNode', function() { describe('hasAttributes', function() { it('should return true if the node has at least one attributes', function() { - const node = new axe.VirtualNode('div', { id: 'myDiv' }); + var node = new axe.VirtualNode('div', { id: 'myDiv' }); assert.isTrue(node.hasAttributes()); }); it('should return false if the node has no attributes', function() { - const node = new axe.VirtualNode('div'); + var node = new axe.VirtualNode('div'); assert.isFalse(node.hasAttributes()); }); }); describe('hasAttributes', function() { it('should return true if the node has at least one attributes', function() { - const node = new axe.VirtualNode('div', { id: 'myDiv' }); + var node = new axe.VirtualNode('div', { id: 'myDiv' }); assert.isTrue(node.hasAttributes()); }); it('should return false if the node has no attributes', function() { - const node = new axe.VirtualNode('div'); + var node = new axe.VirtualNode('div'); assert.isFalse(node.hasAttributes()); }); }); describe('matches', function() { - let qsa = axe.utils.querySelectorAll; + var qsa = axe.utils.querySelectorAll; afterEach(function() { axe.utils.querySelectorAll = qsa; }); it('should call axe.utils.querySelectorAll', function() { - const node = new axe.VirtualNode('div', { id: 'myDiv' }); - let success = false; + var node = new axe.VirtualNode('div', { id: 'myDiv' }); + var success = false; axe.utils.querySelectorAll = function() { success = true; return []; @@ -142,7 +142,7 @@ describe('axe.VirtualNode', function() { }); it('should pass an axe virtual node with the node as the actualNode', function() { - const node = new axe.VirtualNode('div', { id: 'myDiv' }); + var node = new axe.VirtualNode('div', { id: 'myDiv' }); axe.utils.querySelectorAll = function(virtualNode) { assert.equal(virtualNode.actualNode, node); return []; @@ -151,7 +151,7 @@ describe('axe.VirtualNode', function() { }); it('should pass the selector', function() { - const node = new axe.VirtualNode('div', { id: 'myDiv' }); + var node = new axe.VirtualNode('div', { id: 'myDiv' }); axe.utils.querySelectorAll = function(virtualNode, selector) { assert.equal(selector, '[id="myDiv"]'); return []; @@ -160,32 +160,32 @@ describe('axe.VirtualNode', function() { }); it('should return a boolean', function() { - const node = new axe.VirtualNode('div', { id: 'myDiv' }); + var node = new axe.VirtualNode('div', { id: 'myDiv' }); axe.utils.querySelectorAll = function() { return [1]; }; - let result = node.matches('[id="myDiv"]'); + var result = node.matches('[id="myDiv"]'); assert.isTrue(result); }); }); describe('contains', function() { it('should return true if the node is equal', function() { - const node = new axe.VirtualNode('div', { id: 'myDiv' }); + var node = new axe.VirtualNode('div', { id: 'myDiv' }); assert.isTrue(node.contains(node)); }); }); describe('cloneNode', function() { it('should return itself', function() { - const node = new axe.VirtualNode('div', { id: 'myDiv' }); + var node = new axe.VirtualNode('div', { id: 'myDiv' }); assert.equal(node, node.cloneNode()); }); }); describe('outerHTML', function() { it('should be a function', function() { - const node = new axe.VirtualNode('div', { id: 'myDiv' }); + var node = new axe.VirtualNode('div', { id: 'myDiv' }); assert.equal(typeof node.outerHTML, 'function'); }); }); From 743a666981d00ca0e7176f7f376b2bc85e193dad Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Thu, 4 Apr 2019 15:30:02 -0600 Subject: [PATCH 4/6] backtrack and just add rule.runSync code path --- lib/core/public/run-virtual-rule.js | 33 ----- lib/core/public/virtual-node.js | 91 ------------- test/core/public/run-virtual-rule.js | 147 -------------------- test/core/public/virtual-node.js | 192 --------------------------- 4 files changed, 463 deletions(-) delete mode 100644 lib/core/public/run-virtual-rule.js delete mode 100644 lib/core/public/virtual-node.js delete mode 100644 test/core/public/run-virtual-rule.js delete mode 100644 test/core/public/virtual-node.js diff --git a/lib/core/public/run-virtual-rule.js b/lib/core/public/run-virtual-rule.js deleted file mode 100644 index 3b958c12e7..0000000000 --- a/lib/core/public/run-virtual-rule.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Run a rule in a non-browser environment - * @param {String} ruleId Id of the rule - * @param {Node} node The virtual node to run the rule against - * @param {Object} options (optional) Set of options passed into rules or checks - * @returns {Object} axe results for the rule run - */ -axe.runVirtualRule = function(ruleId, node, options) { - 'use strict'; - let rule = axe._audit.rules.find(rule => rule.id === ruleId); - - if (!rule) { - return null; - } - - // rule.prototype.gather will try to call axe.utils.isHidden if the - // rule does not exclude hidden elements. This function tries to call - // window.getComputedStyle, so we can avoid this call by forcing the - // rule to not exclude hidden elements - rule.excludeHidden = false; - - const context = { - include: [ - { - actualNode: node - } - ] - }; - - const results = rule.runSync(context, options); - axe.utils.publishMetaData(results); - return results; -}; diff --git a/lib/core/public/virtual-node.js b/lib/core/public/virtual-node.js deleted file mode 100644 index cf472fdf84..0000000000 --- a/lib/core/public/virtual-node.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * A virtual attribute used for virtual nodes. - * @param {String} name Attribute name - * @param {String} value Attribute value - */ -class VirtualAttribute { - constructor(name, value) { - this.name = name; - this.value = value; - } -} - -/** - * A virtual node implementation of Element and Node apis that can - * be used by axe in non-browser environments. - * @param {String} type Name of the node - * @param {Object} attributes Attributes of the node - */ -axe.VirtualNode = class VirtualNode { - constructor(type, attributes = {}) { - this.nodeName = type.toUpperCase(); - this.nodeType = 1; - - this.attributes = { - length: Object.keys(attributes).length - }; - Object.keys(attributes).forEach((name, index) => { - const attribute = new VirtualAttribute(name, attributes[name]); - this.attributes[name] = attribute; - this.attributes[index] = attribute; - }); - - this.tabIndex = this.hasAttribute('tabindex') - ? this.getAttribute('tabindex') - : -1; - } - - getAttribute(attributeName) { - if (typeof attributeName !== 'string') { - return null; - } - - let attribute = this.attributes[attributeName]; - - if (!attribute) { - return null; - } - - return attribute.value; - } - - hasAttribute(attributeName) { - if (typeof attributeName !== 'string') { - return false; - } - - return !!this.attributes[attributeName]; - } - - hasAttributes() { - return this.attributes.length > 0; - } - - // used by axe.utils.select to find nodes that match the rules - // selector. We can reroute the function to call - // axe.utils.querySelectorAll, which doesn’t rely on any browser apis - // to run. However, it gets passed an Axe virtual node, so we need to - // mock that as well - matches(selector) { - return ( - axe.utils.querySelectorAll({ actualNode: this }, selector).length > 0 - ); - } - - // used by axe.utils.contains to check if the node is in context - contains(node) { - return this === node; - } - - // used by axe.utils.getNodeAttributes when the attributes object is - // not of type NamedNodeMap. Since we're a virtual node and won't - // have the problem of being clobbered, we can just return the node - cloneNode() { - return this; - } - - // used by DqElement to get the source. Must exist so the - // XMLSerializer doesn’t get called with the node (which would throw - // an error) - outerHTML() {} -}; diff --git a/test/core/public/run-virtual-rule.js b/test/core/public/run-virtual-rule.js deleted file mode 100644 index f6806d1089..0000000000 --- a/test/core/public/run-virtual-rule.js +++ /dev/null @@ -1,147 +0,0 @@ -describe('axe.runVirtualRule', function() { - 'use strict'; - var audit = axe._audit; - - beforeEach(function() { - axe._audit = { - data: {} - }; - }); - - afterEach(function() { - axe._audit = audit; - }); - - it('should return null if the rule does not exist', function() { - axe._audit.rules = []; - assert.equal(axe.runVirtualRule('aria-roles'), null); - }); - - it('should modify the rule to not excludeHidden', function() { - axe._audit.rules = [ - { - id: 'aria-roles', - excludeHidden: true, - runSync: function() { - return { - id: 'aria-roles', - nodes: [] - }; - } - } - ]; - - axe.runVirtualRule('aria-roles'); - assert.isFalse(axe._audit.rules[0].excludeHidden); - }); - - it('should call rule.runSync', function() { - var success = false; - axe._audit.rules = [ - { - id: 'aria-roles', - runSync: function() { - success = true; - return { - id: 'aria-roles', - nodes: [] - }; - } - } - ]; - - axe.runVirtualRule('aria-roles'); - assert.isTrue(success); - }); - - it('should pass a virtual context to rule.runSync', function() { - axe._audit.rules = [ - { - id: 'aria-roles', - runSync: function(context) { - assert.equal(typeof context, 'object'); - assert.isTrue(Array.isArray(context.include)); - assert.equal(typeof context.include[0], 'object'); - - return { - id: 'aria-roles', - nodes: [] - }; - } - } - ]; - - axe.runVirtualRule('aria-roles'); - }); - - it('should pass the virtualNode as the actualNode of the context', function() { - var node = {}; - axe._audit.rules = [ - { - id: 'aria-roles', - runSync: function(context) { - assert.equal(context.include[0].actualNode, node); - - return { - id: 'aria-roles', - nodes: [] - }; - } - } - ]; - - axe.runVirtualRule('aria-roles', node); - }); - - it('should pass through options to rule.runSync', function() { - axe._audit.rules = [ - { - id: 'aria-roles', - runSync: function(context, options) { - assert.equal(options.foo, 'bar'); - - return { - id: 'aria-roles', - nodes: [] - }; - } - } - ]; - - axe.runVirtualRule('aria-roles', null, { foo: 'bar' }); - }); - - it('should pass the results of rule.runSync to axe.utils.publishMetaData', function() { - var publishMetaData = axe.utils.publishMetaData; - axe.utils.publishMetaData = function(results) { - assert.isTrue(results); - }; - axe._audit.rules = [ - { - id: 'aria-roles', - runSync: function() { - return true; - } - } - ]; - - axe.runVirtualRule('aria-roles'); - axe.utils.publishMetaData = publishMetaData; - }); - - it('should return the results of rule.runSync', function() { - var publishMetaData = axe.utils.publishMetaData; - axe.utils.publishMetaData = function() {}; - axe._audit.rules = [ - { - id: 'aria-roles', - runSync: function() { - return true; - } - } - ]; - - assert.isTrue(axe.runVirtualRule('aria-roles')); - axe.utils.publishMetaData = publishMetaData; - }); -}); diff --git a/test/core/public/virtual-node.js b/test/core/public/virtual-node.js deleted file mode 100644 index 0add55d05e..0000000000 --- a/test/core/public/virtual-node.js +++ /dev/null @@ -1,192 +0,0 @@ -describe('axe.VirtualNode', function() { - 'use strict'; - - it('should return an object', function() { - var node = new axe.VirtualNode('div'); - assert.equal(typeof node, 'object'); - }); - - it('should have a nodeName', function() { - var node = new axe.VirtualNode('div'); - assert.equal(node.nodeName, 'DIV'); - }); - - it('should have a nodeType', function() { - var node = new axe.VirtualNode('div'); - assert.equal(node.nodeType, 1); - }); - - it('should have attributes', function() { - var node = new axe.VirtualNode('div'); - assert.equal(typeof node.attributes, 'object'); - assert.equal(node.attributes.length, 0); - }); - - it('should allow passing an attribute', function() { - var node = new axe.VirtualNode('div', { id: 'myDiv' }); - assert.equal(node.attributes.length, 1); - var id = node.attributes.id; - assert.equal(node.attributes[0], id); - assert.equal(typeof id, 'object'); - assert.equal(id.name, 'id'); - assert.equal(id.value, 'myDiv'); - }); - - it('should allow passing multiple attributes', function() { - var node = new axe.VirtualNode('button', { - id: 'myButton', - type: 'button', - 'aria-label': 'My Button', - class: 'btn btn-primary' - }); - assert.equal(node.attributes.length, 4); - - for (var i = 0; i < node.attributes.length; i++) { - var attribute = node.attributes[i]; - assert.equal(attribute, node.attributes[attribute.name]); - } - }); - - it('should have a tabindex', function() { - var node = new axe.VirtualNode('div'); - assert.equal(node.tabIndex, -1); - }); - - it('should use the tabindex attribute if it exists', function() { - var node = new axe.VirtualNode('div', { tabindex: 10 }); - assert.equal(node.tabIndex, 10); - }); - - describe('getAttribute', function() { - it('should return the attribute value', function() { - var node = new axe.VirtualNode('div', { id: 'myDiv' }); - assert.equal(node.getAttribute('id'), 'myDiv'); - }); - - it('should return null if the attribute does not exist', function() { - var node = new axe.VirtualNode('div', { id: 'myDiv' }); - assert.equal(node.getAttribute('tabindex'), null); - }); - - it('should return null if passed bad data', function() { - var node = new axe.VirtualNode('div', { id: 'myDiv' }); - assert.equal(node.getAttribute(0), null); - assert.equal(node.getAttribute(null), null); - assert.equal(node.getAttribute([]), null); - assert.equal(node.getAttribute({}), null); - assert.equal(node.getAttribute(true), null); - }); - }); - - describe('hasAttribute', function() { - it('should return true if the attribute exists', function() { - var node = new axe.VirtualNode('div', { id: 'myDiv' }); - assert.isTrue(node.hasAttribute('id')); - }); - - it('should return false if the attribute does not exist', function() { - var node = new axe.VirtualNode('div'); - assert.isFalse(node.hasAttribute('tabindex')); - }); - - it('should return false if passed bad data', function() { - var node = new axe.VirtualNode('div', { id: 'myDiv' }); - assert.isFalse(node.hasAttribute(null)); - assert.isFalse(node.hasAttribute([])); - assert.isFalse(node.hasAttribute({})); - assert.isFalse(node.hasAttribute(true)); - assert.isFalse(node.hasAttribute(0)); - }); - }); - - describe('hasAttributes', function() { - it('should return true if the node has at least one attributes', function() { - var node = new axe.VirtualNode('div', { id: 'myDiv' }); - assert.isTrue(node.hasAttributes()); - }); - - it('should return false if the node has no attributes', function() { - var node = new axe.VirtualNode('div'); - assert.isFalse(node.hasAttributes()); - }); - }); - - describe('hasAttributes', function() { - it('should return true if the node has at least one attributes', function() { - var node = new axe.VirtualNode('div', { id: 'myDiv' }); - assert.isTrue(node.hasAttributes()); - }); - - it('should return false if the node has no attributes', function() { - var node = new axe.VirtualNode('div'); - assert.isFalse(node.hasAttributes()); - }); - }); - - describe('matches', function() { - var qsa = axe.utils.querySelectorAll; - - afterEach(function() { - axe.utils.querySelectorAll = qsa; - }); - - it('should call axe.utils.querySelectorAll', function() { - var node = new axe.VirtualNode('div', { id: 'myDiv' }); - var success = false; - axe.utils.querySelectorAll = function() { - success = true; - return []; - }; - node.matches('div'); - assert.isTrue(success); - }); - - it('should pass an axe virtual node with the node as the actualNode', function() { - var node = new axe.VirtualNode('div', { id: 'myDiv' }); - axe.utils.querySelectorAll = function(virtualNode) { - assert.equal(virtualNode.actualNode, node); - return []; - }; - node.matches('div'); - }); - - it('should pass the selector', function() { - var node = new axe.VirtualNode('div', { id: 'myDiv' }); - axe.utils.querySelectorAll = function(virtualNode, selector) { - assert.equal(selector, '[id="myDiv"]'); - return []; - }; - node.matches('[id="myDiv"]'); - }); - - it('should return a boolean', function() { - var node = new axe.VirtualNode('div', { id: 'myDiv' }); - axe.utils.querySelectorAll = function() { - return [1]; - }; - var result = node.matches('[id="myDiv"]'); - assert.isTrue(result); - }); - }); - - describe('contains', function() { - it('should return true if the node is equal', function() { - var node = new axe.VirtualNode('div', { id: 'myDiv' }); - assert.isTrue(node.contains(node)); - }); - }); - - describe('cloneNode', function() { - it('should return itself', function() { - var node = new axe.VirtualNode('div', { id: 'myDiv' }); - assert.equal(node, node.cloneNode()); - }); - }); - - describe('outerHTML', function() { - it('should be a function', function() { - var node = new axe.VirtualNode('div', { id: 'myDiv' }); - assert.equal(typeof node.outerHTML, 'function'); - }); - }); -}); From 452521d4fcfc2cfe7852edd37c6e6a1c2de96ad5 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Mon, 15 Apr 2019 11:02:03 -0600 Subject: [PATCH 5/6] update to es6 --- lib/core/base/check.js | 70 ++++++++++++++++++++---------------------- lib/core/base/rule.js | 4 +-- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/lib/core/base/check.js b/lib/core/base/check.js index 0868cf772b..1e5f419f1d 100644 --- a/lib/core/base/check.js +++ b/lib/core/base/check.js @@ -111,50 +111,46 @@ Check.prototype.run = function(node, options, context, resolve, reject) { * @param {Object} options The options that override the defaults and provide additional * information for the check */ -Check.prototype.runSync = function(node, options, context) { +Check.prototype.runSync = function(node, options = {}, context) { /* eslint max-statements: ["error", 17] */ - 'use strict'; - options = options || {}; - var enabled = options.hasOwnProperty('enabled') - ? options.enabled - : this.enabled, - checkOptions = options.options || this.options; + const { enabled = this.enabled } = options; - if (enabled) { - var checkResult = new CheckResult(this); - var checkHelper = axe.utils.checkHelper(checkResult, options); + if (!enabled) { + return null; + } - // 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'); - }; + const checkOptions = options.options || this.options; + const checkResult = new CheckResult(this); + const checkHelper = axe.utils.checkHelper(checkResult, options); - var result; + // 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'); + }; - 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) { - // Save a reference to the node we errored on for futher debugging. - e.errorNode = new DqElement(node.actualNode).toJSON(); - } - throw e; - } + let result; - checkResult.result = result; - return checkResult; - } else { - return null; + 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) { + // 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; }; /** diff --git a/lib/core/base/rule.js b/lib/core/base/rule.js index d387646296..e5bce5de63 100644 --- a/lib/core/base/rule.js +++ b/lib/core/base/rule.js @@ -145,8 +145,8 @@ Rule.prototype.runChecksSync = function(type, node, options, context) { let results = []; this[type].forEach(function(c) { - var check = self._audit.checks[c.id || c]; - var option = axe.utils.getCheckOption(check, self.id, options); + const check = self._audit.checks[c.id || c]; + const option = axe.utils.getCheckOption(check, self.id, options); results.push(check.runSync(node, option, context)); }); From 7a6e3ec04a4946e0344a80b62a59653a9a4aa03c Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Tue, 7 May 2019 15:45:20 -0600 Subject: [PATCH 6/6] add test for vitualNode, put log functions on rule --- lib/core/base/rule.js | 36 +++++++++++++++++++----------------- test/core/base/check.js | 19 +++++++++++++++++++ test/core/base/rule.js | 12 ++++++------ 3 files changed, 44 insertions(+), 23 deletions(-) diff --git a/lib/core/base/rule.js b/lib/core/base/rule.js index c177bf8017..8fa9869aa0 100644 --- a/lib/core/base/rule.js +++ b/lib/core/base/rule.js @@ -213,7 +213,7 @@ Rule.prototype.run = function(context, options = {}, resolve, reject) { } if (options.performanceTimer) { - logGatherPerformance(this, nodes); + this._logGatherPerformance(nodes); } nodes.forEach(node => { @@ -244,7 +244,7 @@ Rule.prototype.run = function(context, options = {}, resolve, reject) { q.defer(resolve => setTimeout(resolve, 0)); if (options.performanceTimer) { - logRulePerformance(this); + this._logRulePerformance(); } q.then(() => resolve(ruleResult)).catch(error => reject(error)); @@ -271,7 +271,7 @@ Rule.prototype.runSync = function(context, options = {}) { } if (options.performanceTimer) { - logGatherPerformance(this, nodes); + this._logGatherPerformance(nodes); } nodes.forEach(node => { @@ -288,7 +288,7 @@ Rule.prototype.runSync = function(context, options = {}) { }); if (options.performanceTimer) { - logRulePerformance(this); + this._logRulePerformance(); } return ruleResult; @@ -307,38 +307,40 @@ Rule.prototype._trackPerformance = function() { /** * Log performance of rule.gather + * @private * @param {Rule} rule The rule to log * @param {Array} nodes Result of rule.gather */ -function logGatherPerformance(rule, nodes) { +Rule.prototype._logGatherPerformance = function(nodes) { axe.log( 'gather (', nodes.length, '):', axe.utils.performanceTimer.timeElapsed() + 'ms' ); - axe.utils.performanceTimer.mark(rule._markChecksStart); -} + axe.utils.performanceTimer.mark(this._markChecksStart); +}; /** * Log performance of the rule + * @private * @param {Rule} rule The rule to log */ -function logRulePerformance(rule) { - axe.utils.performanceTimer.mark(rule._markChecksEnd); - axe.utils.performanceTimer.mark(rule._markEnd); +Rule.prototype._logRulePerformance = function() { + axe.utils.performanceTimer.mark(this._markChecksEnd); + axe.utils.performanceTimer.mark(this._markEnd); axe.utils.performanceTimer.measure( - 'runchecks_' + rule.id, - rule._markChecksStart, - rule._markChecksEnd + 'runchecks_' + this.id, + this._markChecksStart, + this._markChecksEnd ); axe.utils.performanceTimer.measure( - 'rule_' + rule.id, - rule._markStart, - rule._markEnd + 'rule_' + this.id, + this._markStart, + this._markEnd ); -} +}; /** * Process the results of each check and return the result if a check diff --git a/test/core/base/check.js b/test/core/base/check.js index a19334646d..278fb33a41 100644 --- a/test/core/base/check.js +++ b/test/core/base/check.js @@ -184,6 +184,16 @@ describe('Check', function() { }).run(fixture, {}, configured, noop); }); + it('should pass the virtual node through', function(done) { + var tree = axe.utils.getFlattenedTree(fixture); + new Check({ + evaluate: function(node, options, virtualNode) { + assert.equal(virtualNode, tree[0]); + done(); + } + }).run(tree[0]); + }); + it('should bind context to `bindCheckResult`', function(done) { var orig = axe.utils.checkHelper, cb = function() { @@ -326,6 +336,15 @@ describe('Check', function() { }).runSync(fixture, {}, configured); }); + it('should pass the virtual node through', function() { + var tree = axe.utils.getFlattenedTree(fixture); + new Check({ + evaluate: function(node, options, virtualNode) { + assert.equal(virtualNode, tree[0]); + } + }).runSync(tree[0]); + }); + it('should throw error for asynchronous checks', function() { var data = { monkeys: 'bananas' }; diff --git a/test/core/base/rule.js b/test/core/base/rule.js index ca297bf15d..a1ec2497aa 100644 --- a/test/core/base/rule.js +++ b/test/core/base/rule.js @@ -842,12 +842,12 @@ describe('Rule', function() { var div = document.createElement('div'); div.setAttribute('style', '#fff'); fixture.appendChild(div); - var success = false, - rule = new Rule({ - matches: function() { - throw new Error('this is an error'); - } - }); + var success = false; + var rule = new Rule({ + matches: function() { + throw new Error('this is an error'); + } + }); try { rule.runSync(