diff --git a/doc/API.md b/doc/API.md index 971298d23e..dfb7fccba6 100644 --- a/doc/API.md +++ b/doc/API.md @@ -376,6 +376,14 @@ Additionally, there are a number or properties that allow configuration of diffe } ``` + Alternatively, runOnly can be passed an array of tags: + + ```javascript + { + runOnly: ["wcag2a", "wcag2aa"] + } + ``` + 2. Run only a specified list of Rules If you only want to run certain rules, specify options as: diff --git a/lib/core/base/audit.js b/lib/core/base/audit.js index 240dfe8a74..f3a5a519aa 100644 --- a/lib/core/base/audit.js +++ b/lib/core/base/audit.js @@ -142,7 +142,7 @@ Audit.prototype.addCheck = function (spec) { */ Audit.prototype.run = function (context, options, resolve, reject) { 'use strict'; - this.validateOptions(options); + this.normalizeOptions(options); axe._selectCache = []; var q = axe.utils.queue(); @@ -220,42 +220,53 @@ Audit.prototype.getRule = function (ruleId) { * @param {Object} options Options object * @return {Object} Validated options object */ -Audit.prototype.validateOptions = function (options) { +Audit.prototype.normalizeOptions = function (options) { + /* eslint max-statements: ["error", 22] */ 'use strict'; var audit = this; // Validate runOnly if (typeof options.runOnly === 'object') { - var only = options.runOnly; + if (Array.isArray(options.runOnly)) { + options.runOnly = { + type: 'tag', + values: options.runOnly + }; + } + const only = options.runOnly; + if (only.value && !only.values) { + only.values = only.value; + delete only.value; + } + + if (!Array.isArray(only.values) || only.values.length === 0) { + throw new Error('runOnly.values must be a non-empty array'); + } // Check if every value in options.runOnly is a known rule ID - if (only.type === 'rule' && Array.isArray(only.value)) { - only.value.forEach(function (ruleId) { + if (['rule', 'rules'].includes(only.type)) { + only.type = 'rule'; + only.values.forEach(function (ruleId) { if (!audit.getRule(ruleId)) { throw new Error('unknown rule `' + ruleId + '` in options.runOnly'); } }); // Validate 'tags' (e.g. anything not 'rule') - } else if (Array.isArray(only.value) && only.value.length > 0) { - var tags = [].concat(only.value); - - audit.rules.forEach(function (rule) { - var tagPos, i, l; - if (!tags) { - return; - } - // Remove any known tag - for (i = 0, l = rule.tags.length; i < l; i++) { - tagPos = tags.indexOf(rule.tags[i]); - if (tagPos !== -1) { - tags.splice(tagPos, 1); - } - } - }); - if (tags.length !== 0) { - throw new Error('could not find tags `' + tags.join('`, `') + '`'); + } else if (['tag', 'tags', undefined].includes(only.type)) { + only.type = 'tag'; + const unmatchedTags = audit.rules.reduce((unmatchedTags, rule) => { + return (unmatchedTags.length + ? unmatchedTags.filter(tag => !rule.tags.includes(tag)) + : unmatchedTags + ); + }, only.values); + + if (unmatchedTags.length !== 0) { + throw new Error('Could not find tags `' + unmatchedTags.join('`, `') + '`'); } + } else { + throw new Error(`Unknown runOnly type '${only.type}'`); } } diff --git a/test/core/base/audit.js b/test/core/base/audit.js index 8e924f200f..9ed7124515 100644 --- a/test/core/base/audit.js +++ b/test/core/base/audit.js @@ -32,20 +32,24 @@ describe('Audit', function () { var mockRules = [{ id: 'positive1', selector: 'input', + tags: ['positive'], any: [{ id: 'positive1-check1', }] }, { id: 'positive2', selector: '#monkeys', + tags: ['positive'], any: ['positive2-check1'] }, { id: 'negative1', selector: 'div', + tags: ['negative'], none: ['negative1-check1'] }, { id: 'positive3', selector: 'blink', + tags: ['positive'], any: ['positive3-check1'] }]; @@ -621,14 +625,14 @@ describe('Audit', function () { }, isNotCalled); }); - it('should run audit.validateOptions to ensure valid input', function () { + it('should run audit.normalizeOptions to ensure valid input', function () { fixture.innerHTML = '' + '
bananas
' + '' + 'FAIL ME'; var checked = 'options not validated'; - a.validateOptions = function () { + a.normalizeOptions = function () { checked = 'options validated'; }; @@ -687,27 +691,108 @@ describe('Audit', function () { }); }); - describe('Audit#validateOptions', function () { + describe('Audit#normalizeOptions', function () { it('returns the options object when it is valid', function () { var opt = { runOnly: { type: 'rule', - value: ['positive1', 'positive2'] + values: ['positive1', 'positive2'] }, rules: { negative1: { enabled: false } } }; - assert(a.validateOptions(opt), opt); + assert(a.normalizeOptions(opt), opt); + }); + + it('allows `value` as alternative to `values`', function () { + var opt = { + runOnly: { + type: 'rule', + value: ['positive1', 'positive2'] + } + }; + var out = a.normalizeOptions(opt) + assert.deepEqual(out.runOnly.values, ['positive1', 'positive2']); + assert.isUndefined(out.runOnly.value); + }); + + it('allows type: rules as an alternative to type: rule', function () { + var opt = { + runOnly: { + type: 'rules', + values: ['positive1', 'positive2'] + } + }; + assert(a.normalizeOptions(opt).runOnly.type, 'rule'); + }); + + it('allows type: tags as an alternative to type: tag', function () { + var opt = { + runOnly: { + type: 'tags', + values: ['positive'] + } + }; + assert(a.normalizeOptions(opt).runOnly.type, 'tag'); + }); + + it('allows type: undefined as an alternative to type: tag', function () { + var opt = { + runOnly: { + values: ['positive'] + } + }; + assert(a.normalizeOptions(opt).runOnly.type, 'tag'); + }); + + it('allows runOnly as an array as an alternative to type: tag', function () { + var opt = { runOnly: ['positive', 'negative'] }; + var out = a.normalizeOptions(opt); + assert(out.runOnly.type, 'tag'); + assert.deepEqual(out.runOnly.values, ['positive', 'negative']); + }); + + it('throws an error runOnly.values not an array', function () { + assert.throws(function () { + a.normalizeOptions({ + runOnly: { + type: 'rule', + values: { badProp: 'badValue' } + } + }); + }); + }); + + it('throws an error runOnly.values an empty', function () { + assert.throws(function () { + a.normalizeOptions({ + runOnly: { + type: 'rule', + values: [] + } + }); + }); + }); + + it('throws an error runOnly.type is unknown', function () { + assert.throws(function () { + a.normalizeOptions({ + runOnly: { + type: 'something-else', + values: ['wcag2aa'] + } + }); + }); }); it('throws an error when option.runOnly has an unknown rule', function () { assert.throws(function () { - a.validateOptions({ + a.normalizeOptions({ runOnly: { type: 'rule', - value: ['frakeRule'] + values: ['frakeRule'] } }); }); @@ -715,10 +800,10 @@ describe('Audit', function () { it('throws an error when option.runOnly has an unknown tag', function () { assert.throws(function () { - a.validateOptions({ + a.normalizeOptions({ runOnly: { type: 'tags', - value: ['fakeTag'] + values: ['fakeTag'] } }); }); @@ -726,7 +811,7 @@ describe('Audit', function () { it('throws an error when option.rules has an unknown rule', function () { assert.throws(function () { - a.validateOptions({ + a.normalizeOptions({ rules: { fakeRule: { enabled: false} }