From 78543b4a2d8c1bcc77784ea8369a5af997b428ad Mon Sep 17 00:00:00 2001 From: Matt Isner Date: Tue, 10 Oct 2017 12:10:17 -0400 Subject: [PATCH 1/2] perf(reporter): add option to limit result types to be processed Closes #512 https://github.com/dequelabs/axe-core/issues/512 --- .../reporters/helpers/process-aggregate.js | 28 ++ lib/core/reporters/no-passes.js | 3 + .../reporters/helpers/process-aggregate.js | 246 ++++++++++++++++++ 3 files changed, 277 insertions(+) create mode 100644 test/core/reporters/helpers/process-aggregate.js diff --git a/lib/core/reporters/helpers/process-aggregate.js b/lib/core/reporters/helpers/process-aggregate.js index f72f5e9991..e124df749d 100644 --- a/lib/core/reporters/helpers/process-aggregate.js +++ b/lib/core/reporters/helpers/process-aggregate.js @@ -27,6 +27,30 @@ function normalizeRelatedNodes(node, options) { } var resultKeys = axe.constants.resultGroups; + +/** + * Configures the processing of axe results. + * + * @typedef ProcessOptions + * @property {Array} resultsTypes limit the types of results to process ('passes', 'violations', etc.) + * @property {Boolean} elementRef display node's element references + * @property {Boolean} selectors display node's selectors + * @property {Boolean} xpath display node's xpaths + */ + +/** + * Aggregrate and process the aXe results, + * adding desired data to nodes and relatedNodes in each rule result. + * + * Prepares result data for reporters. + * + * @method processAggregate + * @memberof helpers + * @param {Array} results + * @param {ProcessOptions} options + * @return {Object} + * + */ helpers.processAggregate = function (results, options) { var resultObject = axe.utils.aggregateResult(results); @@ -34,6 +58,10 @@ helpers.processAggregate = function (results, options) { resultObject.url = window.location.href; resultKeys.forEach(function (key) { + if (options.resultTypes && !options.resultTypes.includes(key)) { + delete resultObject[key]; + return; + } resultObject[key] = (resultObject[key] || []).map(function (ruleResult) { ruleResult = Object.assign({}, ruleResult); diff --git a/lib/core/reporters/no-passes.js b/lib/core/reporters/no-passes.js index be32c2d956..f23b2f0d12 100644 --- a/lib/core/reporters/no-passes.js +++ b/lib/core/reporters/no-passes.js @@ -5,6 +5,9 @@ axe.addReporter('no-passes', function (results, options, callback) { callback = options; options = {}; } + // limit result processing to types we want to include in the output + options.resultTypes = ['violations']; + var out = helpers.processAggregate(results, options); callback({ diff --git a/test/core/reporters/helpers/process-aggregate.js b/test/core/reporters/helpers/process-aggregate.js new file mode 100644 index 0000000000..0a62c066a5 --- /dev/null +++ b/test/core/reporters/helpers/process-aggregate.js @@ -0,0 +1,246 @@ + +describe('helpers.processAggregate', function () { + 'use strict'; + var results, options; + + beforeEach(function () { + results = [{ + id: 'passed-rule', + passes: [{ + result: 'passed', + node: { + element: document.createElement('div'), + selector: 'header > .thing', + source: '
Thing
', + xpath: '/header/div[@class="thing"]' + }, + any: [{ + id: 'passed-rule', + relatedNodes: [{ + element: document.createElement('div'), + selector: 'footer > .thing', + source: '
Thing
', + xpath: '/footer/div[@class="thing"]', + }] + }], + all: [], + none: [] + }], + inapplicable: [], + incomplete: [], + violations: [] + }, { + id: 'failed-rule', + violations: [{ + result: 'failed', + node: { + selector: '#dopel', + source: '', + xpath: '/main/input[@id="dopel"]', + fromFrame: true + }, + any: [{ + id: 'failed-rule', + relatedNodes: [{ + element: document.createElement('input'), + selector: '#dopel', + source: '', + xpath: '/main/input[@id="dopel"]', + fromFrame: true + }] + }], + all: [], + none: [] + }], + inapplicable: [], + passes: [], + incomplete: [] + }]; + }); + + it('should add a `timestamp` property to the `resultObject`', function () { + var resultObject = helpers.processAggregate(results, {}); + assert.isDefined(resultObject.timestamp); + }); + + it('should add a `url` property to the `resultObject`', function () { + var resultObject = helpers.processAggregate(results, {}); + assert.isDefined(resultObject.url); + }); + + it('should remove the `result` property from each node in each ruleResult', function () { + assert.isDefined(results.find(function (r) { + return r.id === 'passed-rule'; + }).passes[0].result); + + var resultObject = helpers.processAggregate(results, {}); + var ruleResult = resultObject.passes.find(function (r) { + return r.id === 'passed-rule'; + }); + assert.isUndefined(ruleResult.nodes[0].result); + }); + + it('should remove the `node` property from each node in each ruleResult', function () { + assert.isDefined(results.find(function (r) { + return r.id === 'passed-rule'; + }).passes[0].node); + + var resultObject = helpers.processAggregate(results, {}); + var ruleResult = resultObject.passes.find(function (r) { + return r.id === 'passed-rule'; + }); + assert.isUndefined(ruleResult.nodes[0].node); + }); + + describe('`options` argument', function () { + + describe('`resultTypes` option', function () { + + it('should remove non-specified result types from the `resultObject`', function () { + var resultObject = helpers.processAggregate(results, { resultTypes: ['passes', 'violations'] }); + assert.isDefined(resultObject.passes); + assert.isDefined(resultObject.violations); + assert.isUndefined(resultObject.incomplete); + assert.isUndefined(resultObject.inapplicable); + }); + }); + + describe('`elementRef` option', function () { + + describe('when set to true', function () { + + before(function () { + options = { elementRef: true }; + }); + + describe('when node\'s, or relatedNode\'s, `fromFrame` equals false', function () { + it('should add an `element` property to the subResult nodes or relatedNodes', function () { + var resultObject = helpers.processAggregate(results, options); + assert.isDefined(resultObject.passes[0].nodes[0].element); + assert.isDefined(resultObject.passes[0].nodes[0].any[0].relatedNodes[0].element); + }); + }); + + describe('when node\'s, or relatedNode\'s, `fromFrame` equals true', function () { + it('should NOT add an `element` property to the subResult nodes or relatedNodes', function () { + var resultObject = helpers.processAggregate(results, options); + assert.isUndefined(resultObject.violations[0].nodes[0].element); + assert.isUndefined(resultObject.violations[0].nodes[0].any[0].relatedNodes[0].element); + }); + }); + }); + + describe('when set to false', function () { + + before(function () { + options = { elementRef: false }; + }); + + it('should NOT add an `element` property to the subResult nodes or relatedNodes', function () { + var resultObject = helpers.processAggregate(results, options); + assert.isUndefined(resultObject.passes[0].nodes[0].element); + assert.isUndefined(resultObject.violations[0].nodes[0].element); + assert.isUndefined(resultObject.passes[0].nodes[0].any[0].relatedNodes[0].element); + assert.isUndefined(resultObject.violations[0].nodes[0].any[0].relatedNodes[0].element); + }); + }); + + describe('when not set at all', function () { + + it('should NOT add an `element` property to the subResult nodes or relatedNodes', function () { + var resultObject = helpers.processAggregate(results, {}); + assert.isUndefined(resultObject.passes[0].nodes[0].element); + assert.isUndefined(resultObject.violations[0].nodes[0].element); + assert.isUndefined(resultObject.passes[0].nodes[0].any[0].relatedNodes[0].element); + assert.isUndefined(resultObject.violations[0].nodes[0].any[0].relatedNodes[0].element); + }); + }); + }); + + describe('`selectors` option', function () { + + describe('when set to false', function () { + + before(function () { + options = { selectors: false }; + }); + + describe('when node\'s, or relatedNode\'s, `fromFrame` equals true', function () { + it('should add a `target` property to the subResult nodes or relatedNodes', function () { + var resultObject = helpers.processAggregate(results, options); + assert.isDefined(resultObject.violations[0].nodes[0].target); + assert.isDefined(resultObject.violations[0].nodes[0].any[0].relatedNodes[0].target); + }); + }); + + describe('when node\'s, or relatedNode\'s, `fromFrame` equals false', function () { + it('should NOT add a `target` property to the subResult nodes or relatedNodes', function () { + var resultObject = helpers.processAggregate(results, options); + assert.isUndefined(resultObject.passes[0].nodes[0].target); + assert.isUndefined(resultObject.passes[0].nodes[0].any[0].relatedNodes[0].target); + }); + }); + }); + + describe('when set to true', function () { + + before(function () { + options = { selectors: true }; + }); + + it('should add a `target` property to the subResult nodes or relatedNodes', function () { + var resultObject = helpers.processAggregate(results, options); + assert.isDefined(resultObject.passes[0].nodes[0].target); + assert.isDefined(resultObject.passes[0].nodes[0].any[0].relatedNodes[0].target); + }); + }); + + describe('when not set at all', function () { + + it('should add a `target` property to the subResult nodes or relatedNodes', function () { + var resultObject = helpers.processAggregate(results, {}); + assert.isDefined(resultObject.passes[0].nodes[0].target); + assert.isDefined(resultObject.passes[0].nodes[0].any[0].relatedNodes[0].target); + }); + }); + }); + + describe('`xpath` option', function () { + + describe('when set to true', function () { + + before(function () { + options = { xpath: true }; + }); + + it('should add an `xpath` property to the subResult nodes or relatedNodes', function () { + var resultObject = helpers.processAggregate(results, options); + assert.isDefined(resultObject.passes[0].nodes[0].xpath); + assert.isDefined(resultObject.passes[0].nodes[0].any[0].relatedNodes[0].xpath); + }); + }); + + describe('when set to false', function () { + + before(function () { + options = { xpath: false }; + }); + + it('should NOT add an `xpath` property to the subResult nodes or relatedNodes', function () { + var resultObject = helpers.processAggregate(results, options); + assert.isUndefined(resultObject.passes[0].nodes[0].xpath); + assert.isUndefined(resultObject.passes[0].nodes[0].any[0].relatedNodes[0].xpath); + }); + }); + + describe('when not set at all', function () { + + it('should NOT add an `xpath` property to the subResult nodes or relatedNodes', function () { + var resultObject = helpers.processAggregate(results, {}); + assert.isUndefined(resultObject.passes[0].nodes[0].xpath); + assert.isUndefined(resultObject.passes[0].nodes[0].any[0].relatedNodes[0].xpath); + }); + }); + }); + }); +}); From 2a5607190badc2a7e893643266654e36adde4238 Mon Sep 17 00:00:00 2001 From: Matt Isner Date: Tue, 17 Oct 2017 13:41:06 -0400 Subject: [PATCH 2/2] docs(resultTypes): add documentation for resultTypes option, and fix a small doc error Related to https://github.com/dequelabs/axe-core/issues/512 --- doc/API.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/API.md b/doc/API.md index 11bfd3d3a1..c7e29471ac 100644 --- a/doc/API.md +++ b/doc/API.md @@ -231,7 +231,7 @@ axe.run(context, options, callback); #### Parameters axe.run * [`context`](#context-parameter): (optional) Defines the scope of the analysis - the part of the DOM that you would like to analyze. This will typically be the `document` or a specific selector such as class name, ID, selector, etc. -* [`options`](#options-parameter): (optional) Set of options passed into rules or checks, temporarily modifying them. This contrasts with `axe.configure`, which is more permanent. [See below for more information](#axerun-parameters) +* [`options`](#options-parameter): (optional) Set of options passed into rules or checks, temporarily modifying them. This contrasts with `axe.configure`, which is more permanent. * [`callback`](#callback-parameter): (optional) The callback function which receives either null or an [error result](#error-result) as the first parameter, and the [results object](#results-object) when analysis is completed successfully, or undefined if it did not. ##### Context Parameter @@ -331,6 +331,7 @@ Additionally, there are a number or properties that allow configuration of diffe | `runOnly` | n/a | Limit which rules are executed, based on names or tags | `rules` | n/a | Allow customizing a rule's properties (including { enable: false }) | `reporter` | `v1` | Which reporter to use (see [Configuration](#api-name-axeconfigure)) +| `resultTypes` | n/a | Limit which result types are processed and aggregated | `xpath` | `false` | Return xpath selectors for elements | `absolutePaths` | `false` | Use absolute paths when creating element selectors | `iframes` | `true` | Tell axe to run inside iframes @@ -437,6 +438,16 @@ Additionally, there are a number or properties that allow configuration of diffe This example first includes all `wcag2a` and `wcag2aa` rules. All rules that are tagged as `experimental` are than removed from the list of rules to run. +6. Only process certain types of results + + The `resultTypes` option can be used to limit the result types that aXe will process, aggregate, and send to the reporter. This can be useful for improving performance on very large or complicated pages when you are only interested in certain types of results. + ```javascript + { + resultTypes: ['violations', 'incomplete', 'inapplicable'] + } + ``` + This example will only process the specified result types: "violations", "incomplete", and "inapplicable". Notably, it will not process "passes". On a series of extremely large pages, this could improve performance considerably. + ##### Callback Parameter The callback parameter is a function that will be called when the asynchronous `axe.run` function completes. The callback function is passed two parameters. The first parameter will be an error thrown inside of aXe if axe.run could not complete. If axe completed correctly the first parameter will be null, and the second parameter will be the results object.