diff --git a/doc/API.md b/doc/API.md index f357317cd7..bd80acaa9f 100644 --- a/doc/API.md +++ b/doc/API.md @@ -179,17 +179,18 @@ User specifies the format of the JSON structure passed to the callback of `axe.r ```js axe.configure({ - branding: { - brand: String, - application: String - }, - reporter: 'option' | Function, - checks: [Object], - rules: [Object], - standards: Object, - locale: Object, - axeVersion: String, - disableOtherRules: Boolean + branding: { + brand: String, + application: String + }, + reporter: 'option' | Function, + checks: [Object], + rules: [Object], + standards: Object, + locale: Object, + axeVersion: String, + disableOtherRules: Boolean, + noHtml: Boolean }); ``` @@ -232,6 +233,7 @@ axe.configure({ - `disableOtherRules` - Disables all rules not included in the `rules` property. - `locale` - A locale object to apply (at runtime) to all rules and checks, in the same shape as `/locales/*.json`. - `axeVersion` - Set the compatible version of a custom rule with the current axe version. Compatible versions are all patch and minor updates that are the same as, or newer than those of the `axeVersion` property. + - `noHtml` - Disables the HTML output of nodes from rules. **Returns:** Nothing diff --git a/lib/core/base/audit.js b/lib/core/base/audit.js index d9472a8d1e..981f0b7064 100644 --- a/lib/core/base/audit.js +++ b/lib/core/base/audit.js @@ -29,11 +29,12 @@ function getDefaultConfiguration(audit) { config = {}; } - config.reporter = config.reporter || null; - config.rules = config.rules || []; - config.checks = config.checks || []; - config.data = { checks: {}, rules: {}, ...config.data }; - return config; + config.reporter = config.reporter || null; + config.noHtml = config.noHtml || false; + config.rules = config.rules || []; + config.checks = config.checks || []; + config.data = { checks: {}, rules: {}, ...config.data }; + return config; } function unpackToObject(collection, audit, method) { @@ -148,196 +149,197 @@ const mergeFallbackMessage = (a, b) => { * Constructor which holds configured rules and information about the document under test */ class Audit { - constructor(audit) { - // defaults - this.lang = 'en'; - this.defaultConfig = audit; - this.standards = standards; - this._init(); - // A copy of the "default" locale. This will be set if the user - // provides a new locale to `axe.configure()` and used to undo - // changes in `axe.reset()`. - this._defaultLocale = null; - } - /** - * Build and set the previous locale. Will noop if a previous - * locale was already set, as we want the ability to "reset" - * to the default ("first") configuration. - */ - _setDefaultLocale() { - if (this._defaultLocale) { - return; - } - const locale = { - checks: {}, - rules: {}, - failureSummaries: {}, - incompleteFallbackMessage: '', - lang: this.lang - }; - // XXX: unable to use `for-of` here, as doing so would - // require us to polyfill `Symbol`. - const checkIDs = Object.keys(this.data.checks); - for (let i = 0; i < checkIDs.length; i++) { - const id = checkIDs[i]; - const check = this.data.checks[id]; - const { pass, fail, incomplete } = check.messages; - locale.checks[id] = { - pass, - fail, - incomplete - }; - } - const ruleIDs = Object.keys(this.data.rules); - for (let i = 0; i < ruleIDs.length; i++) { - const id = ruleIDs[i]; - const rule = this.data.rules[id]; - const { description, help } = rule; - locale.rules[id] = { description, help }; - } - const failureSummaries = Object.keys(this.data.failureSummaries); - for (let i = 0; i < failureSummaries.length; i++) { - const type = failureSummaries[i]; - const failureSummary = this.data.failureSummaries[type]; - const { failureMessage } = failureSummary; - locale.failureSummaries[type] = { failureMessage }; - } - locale.incompleteFallbackMessage = this.data.incompleteFallbackMessage; - this._defaultLocale = locale; - } - /** - * Reset the locale to the "default". - */ - _resetLocale() { - // If the default locale has not already been set, we can exit early. - const defaultLocale = this._defaultLocale; - if (!defaultLocale) { - return; - } - // Apply the default locale - this.applyLocale(defaultLocale); - } - /** - * Apply locale for the given `checks`. - */ - _applyCheckLocale(checks) { - const keys = Object.keys(checks); - for (let i = 0; i < keys.length; i++) { - const id = keys[i]; - if (!this.data.checks[id]) { - throw new Error(`Locale provided for unknown check: "${id}"`); - } - this.data.checks[id] = mergeCheckLocale(this.data.checks[id], checks[id]); - } - } - /** - * Apply locale for the given `rules`. - */ - _applyRuleLocale(rules) { - const keys = Object.keys(rules); - for (let i = 0; i < keys.length; i++) { - const id = keys[i]; - if (!this.data.rules[id]) { - throw new Error(`Locale provided for unknown rule: "${id}"`); - } - this.data.rules[id] = mergeRuleLocale(this.data.rules[id], rules[id]); - } - } - /** - * Apply locale for the given failureMessage - */ - _applyFailureSummaries(messages) { - const keys = Object.keys(messages); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - if (!this.data.failureSummaries[key]) { - throw new Error(`Locale provided for unknown failureMessage: "${key}"`); - } - this.data.failureSummaries[key] = mergeFailureMessage( - this.data.failureSummaries[key], - messages[key] - ); - } - } - /** - * Apply the given `locale`. - * - * @param {axe.Locale} - */ - applyLocale(locale) { - this._setDefaultLocale(); - if (locale.checks) { - this._applyCheckLocale(locale.checks); - } - if (locale.rules) { - this._applyRuleLocale(locale.rules); - } - if (locale.failureSummaries) { - this._applyFailureSummaries(locale.failureSummaries, 'failureSummaries'); - } - if (locale.incompleteFallbackMessage) { - this.data.incompleteFallbackMessage = mergeFallbackMessage( - this.data.incompleteFallbackMessage, - locale.incompleteFallbackMessage - ); - } - if (locale.lang) { - this.lang = locale.lang; - } - } - /** - * Initializes the rules and checks - */ - _init() { - var audit = getDefaultConfiguration(this.defaultConfig); - this.lang = audit.lang || 'en'; - this.reporter = audit.reporter; - this.commands = {}; - this.rules = []; - this.checks = {}; - this.brand = 'axe'; - this.application = 'axeAPI'; - this.tagExclude = ['experimental']; - unpackToObject(audit.rules, this, 'addRule'); - unpackToObject(audit.checks, this, 'addCheck'); - this.data = {}; - this.data.checks = (audit.data && audit.data.checks) || {}; - this.data.rules = (audit.data && audit.data.rules) || {}; - this.data.failureSummaries = - (audit.data && audit.data.failureSummaries) || {}; - this.data.incompleteFallbackMessage = - (audit.data && audit.data.incompleteFallbackMessage) || ''; - this._constructHelpUrls(); // create default helpUrls - } - /** - * Adds a new command to the audit - */ - registerCommand(command) { - this.commands[command.id] = command.callback; - } - /** - * Adds a new rule to the Audit. If a rule with specified ID already exists, it will be overridden - * @param {Object} spec Rule specification object - */ - addRule(spec) { - if (spec.metadata) { - this.data.rules[spec.id] = spec.metadata; - } - let rule = this.getRule(spec.id); - if (rule) { - rule.configure(spec); - } else { - this.rules.push(new Rule(spec, this)); - } - } - /** - * Adds a new check to the Audit. If a Check with specified ID already exists, it will be - * reconfigured - * - * @param {Object} spec Check specification object - */ - addCheck(spec) { - /*eslint no-eval: 0 */ + constructor(audit) { + // defaults + this.lang = 'en'; + this.defaultConfig = audit; + this.standards = standards; + this._init(); + // A copy of the "default" locale. This will be set if the user + // provides a new locale to `axe.configure()` and used to undo + // changes in `axe.reset()`. + this._defaultLocale = null; + } + /** + * Build and set the previous locale. Will noop if a previous + * locale was already set, as we want the ability to "reset" + * to the default ("first") configuration. + */ + _setDefaultLocale() { + if (this._defaultLocale) { + return; + } + const locale = { + checks: {}, + rules: {}, + failureSummaries: {}, + incompleteFallbackMessage: '', + lang: this.lang + }; + // XXX: unable to use `for-of` here, as doing so would + // require us to polyfill `Symbol`. + const checkIDs = Object.keys(this.data.checks); + for (let i = 0; i < checkIDs.length; i++) { + const id = checkIDs[i]; + const check = this.data.checks[id]; + const { pass, fail, incomplete } = check.messages; + locale.checks[id] = { + pass, + fail, + incomplete + }; + } + const ruleIDs = Object.keys(this.data.rules); + for (let i = 0; i < ruleIDs.length; i++) { + const id = ruleIDs[i]; + const rule = this.data.rules[id]; + const { description, help } = rule; + locale.rules[id] = { description, help }; + } + const failureSummaries = Object.keys(this.data.failureSummaries); + for (let i = 0; i < failureSummaries.length; i++) { + const type = failureSummaries[i]; + const failureSummary = this.data.failureSummaries[type]; + const { failureMessage } = failureSummary; + locale.failureSummaries[type] = { failureMessage }; + } + locale.incompleteFallbackMessage = this.data.incompleteFallbackMessage; + this._defaultLocale = locale; + } + /** + * Reset the locale to the "default". + */ + _resetLocale() { + // If the default locale has not already been set, we can exit early. + const defaultLocale = this._defaultLocale; + if (!defaultLocale) { + return; + } + // Apply the default locale + this.applyLocale(defaultLocale); + } + /** + * Apply locale for the given `checks`. + */ + _applyCheckLocale(checks) { + const keys = Object.keys(checks); + for (let i = 0; i < keys.length; i++) { + const id = keys[i]; + if (!this.data.checks[id]) { + throw new Error(`Locale provided for unknown check: "${id}"`); + } + this.data.checks[id] = mergeCheckLocale(this.data.checks[id], checks[id]); + } + } + /** + * Apply locale for the given `rules`. + */ + _applyRuleLocale(rules) { + const keys = Object.keys(rules); + for (let i = 0; i < keys.length; i++) { + const id = keys[i]; + if (!this.data.rules[id]) { + throw new Error(`Locale provided for unknown rule: "${id}"`); + } + this.data.rules[id] = mergeRuleLocale(this.data.rules[id], rules[id]); + } + } + /** + * Apply locale for the given failureMessage + */ + _applyFailureSummaries(messages) { + const keys = Object.keys(messages); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (!this.data.failureSummaries[key]) { + throw new Error(`Locale provided for unknown failureMessage: "${key}"`); + } + this.data.failureSummaries[key] = mergeFailureMessage( + this.data.failureSummaries[key], + messages[key] + ); + } + } + /** + * Apply the given `locale`. + * + * @param {axe.Locale} + */ + applyLocale(locale) { + this._setDefaultLocale(); + if (locale.checks) { + this._applyCheckLocale(locale.checks); + } + if (locale.rules) { + this._applyRuleLocale(locale.rules); + } + if (locale.failureSummaries) { + this._applyFailureSummaries(locale.failureSummaries, 'failureSummaries'); + } + if (locale.incompleteFallbackMessage) { + this.data.incompleteFallbackMessage = mergeFallbackMessage( + this.data.incompleteFallbackMessage, + locale.incompleteFallbackMessage + ); + } + if (locale.lang) { + this.lang = locale.lang; + } + } + /** + * Initializes the rules and checks + */ + _init() { + var audit = getDefaultConfiguration(this.defaultConfig); + this.lang = audit.lang || 'en'; + this.reporter = audit.reporter; + this.commands = {}; + this.rules = []; + this.checks = {}; + this.brand = 'axe'; + this.application = 'axeAPI'; + this.tagExclude = ['experimental']; + this.noHtml = audit.noHtml; + unpackToObject(audit.rules, this, 'addRule'); + unpackToObject(audit.checks, this, 'addCheck'); + this.data = {}; + this.data.checks = (audit.data && audit.data.checks) || {}; + this.data.rules = (audit.data && audit.data.rules) || {}; + this.data.failureSummaries = + (audit.data && audit.data.failureSummaries) || {}; + this.data.incompleteFallbackMessage = + (audit.data && audit.data.incompleteFallbackMessage) || ''; + this._constructHelpUrls(); // create default helpUrls + } + /** + * Adds a new command to the audit + */ + registerCommand(command) { + this.commands[command.id] = command.callback; + } + /** + * Adds a new rule to the Audit. If a rule with specified ID already exists, it will be overridden + * @param {Object} spec Rule specification object + */ + addRule(spec) { + if (spec.metadata) { + this.data.rules[spec.id] = spec.metadata; + } + const rule = this.getRule(spec.id); + if (rule) { + rule.configure(spec); + } else { + this.rules.push(new Rule(spec, this)); + } + } + /** + * Adds a new check to the Audit. If a Check with specified ID already exists, it will be + * reconfigured + * + * @param {Object} spec Check specification object + */ + addCheck(spec) { + /*eslint no-eval: 0 */ let metadata = spec.metadata; if (typeof metadata === 'object') { diff --git a/lib/core/public/configure.js b/lib/core/public/configure.js index 66cacf5e15..deae724426 100644 --- a/lib/core/public/configure.js +++ b/lib/core/public/configure.js @@ -2,114 +2,118 @@ import { hasReporter } from './reporter'; import { configureStandards } from '../../standards'; function configure(spec) { - var audit; - - audit = axe._audit; - if (!audit) { - throw new Error('No audit configured'); - } - - if (spec.axeVersion || spec.ver) { - let specVersion = spec.axeVersion || spec.ver; - if (!/^\d+\.\d+\.\d+(-canary)?/.test(specVersion)) { - throw new Error(`Invalid configured version ${specVersion}`); - } - - let [version, canary] = specVersion.split('-'); - let [major, minor, patch] = version.split('.').map(Number); - - let [axeVersion, axeCanary] = axe.version.split('-'); - let [axeMajor, axeMinor, axePatch] = axeVersion.split('.').map(Number); - - if ( - major !== axeMajor || - axeMinor < minor || - (axeMinor === minor && axePatch < patch) || - (major === axeMajor && - minor === axeMinor && - patch === axePatch && - canary && - canary !== axeCanary) - ) { - throw new Error( - `Configured version ${specVersion} is not compatible with current axe version ${axe.version}` - ); - } - } - - if ( - spec.reporter && - (typeof spec.reporter === 'function' || hasReporter(spec.reporter)) - ) { - audit.reporter = spec.reporter; - } - - if (spec.checks) { - if (!Array.isArray(spec.checks)) { - throw new TypeError('Checks property must be an array'); - } - - spec.checks.forEach(function(check) { - if (!check.id) { - throw new TypeError( - // eslint-disable-next-line max-len - `Configured check ${JSON.stringify( - check - )} is invalid. Checks must be an object with at least an id property` - ); - } - - audit.addCheck(check); - }); - } - - const modifiedRules = []; - if (spec.rules) { - if (!Array.isArray(spec.rules)) { - throw new TypeError('Rules property must be an array'); - } - - spec.rules.forEach(function(rule) { - if (!rule.id) { - throw new TypeError( - // eslint-disable-next-line max-len - `Configured rule ${JSON.stringify( - rule - )} is invalid. Rules must be an object with at least an id property` - ); - } - - modifiedRules.push(rule.id); - audit.addRule(rule); - }); - } - - if (spec.disableOtherRules) { - audit.rules.forEach(rule => { - if (modifiedRules.includes(rule.id) === false) { - rule.enabled = false; - } - }); - } - - if (typeof spec.branding !== 'undefined') { - audit.setBranding(spec.branding); - } else { - audit._constructHelpUrls(); - } - - if (spec.tagExclude) { - audit.tagExclude = spec.tagExclude; - } - - // Support runtime localization - if (spec.locale) { - audit.applyLocale(spec.locale); - } - - if (spec.standards) { - configureStandards(spec.standards); - } + var audit; + + audit = axe._audit; + if (!audit) { + throw new Error('No audit configured'); + } + + if (spec.axeVersion || spec.ver) { + const specVersion = spec.axeVersion || spec.ver; + if (!/^\d+\.\d+\.\d+(-canary)?/.test(specVersion)) { + throw new Error(`Invalid configured version ${specVersion}`); + } + + const [version, canary] = specVersion.split('-'); + const [major, minor, patch] = version.split('.').map(Number); + + const [axeVersion, axeCanary] = axe.version.split('-'); + const [axeMajor, axeMinor, axePatch] = axeVersion.split('.').map(Number); + + if ( + major !== axeMajor || + axeMinor < minor || + (axeMinor === minor && axePatch < patch) || + (major === axeMajor && + minor === axeMinor && + patch === axePatch && + canary && + canary !== axeCanary) + ) { + throw new Error( + `Configured version ${specVersion} is not compatible with current axe version ${axe.version}` + ); + } + } + + if ( + spec.reporter && + (typeof spec.reporter === 'function' || hasReporter(spec.reporter)) + ) { + audit.reporter = spec.reporter; + } + + if (spec.checks) { + if (!Array.isArray(spec.checks)) { + throw new TypeError('Checks property must be an array'); + } + + spec.checks.forEach(check => { + if (!check.id) { + throw new TypeError( + // eslint-disable-next-line max-len + `Configured check ${JSON.stringify( + check + )} is invalid. Checks must be an object with at least an id property` + ); + } + + audit.addCheck(check); + }); + } + + const modifiedRules = []; + if (spec.rules) { + if (!Array.isArray(spec.rules)) { + throw new TypeError('Rules property must be an array'); + } + + spec.rules.forEach(rule => { + if (!rule.id) { + throw new TypeError( + // eslint-disable-next-line max-len + `Configured rule ${JSON.stringify( + rule + )} is invalid. Rules must be an object with at least an id property` + ); + } + + modifiedRules.push(rule.id); + audit.addRule(rule); + }); + } + + if (spec.disableOtherRules) { + audit.rules.forEach(rule => { + if (modifiedRules.includes(rule.id) === false) { + rule.enabled = false; + } + }); + } + + if (typeof spec.branding !== 'undefined') { + audit.setBranding(spec.branding); + } else { + audit._constructHelpUrls(); + } + + if (spec.tagExclude) { + audit.tagExclude = spec.tagExclude; + } + + // Support runtime localization + if (spec.locale) { + audit.applyLocale(spec.locale); + } + + if (spec.standards) { + configureStandards(spec.standards); + } + + if (spec.noHtml) { + audit.noHtml = true; + } } export default configure; diff --git a/lib/core/utils/dq-element.js b/lib/core/utils/dq-element.js index 118730deec..21df24ef78 100644 --- a/lib/core/utils/dq-element.js +++ b/lib/core/utils/dq-element.js @@ -28,26 +28,32 @@ function getSource(element) { * @param {Object} spec Properties to use in place of the element when instantiated on Elements from other frames */ function DqElement(element, options, spec) { - this._fromFrame = !!spec; - - this.spec = spec || {}; - if (options && options.absolutePaths) { - this._options = { toRoot: true }; - } - - /** - * The generated HTML source code of the element - * @type {String} - */ - this.source = - this.spec.source !== undefined ? this.spec.source : getSource(element); - - /** - * The element which this object is based off or the containing frame, used for sorting. - * Excluded in toJSON method. - * @type {HTMLElement} - */ - this._element = element; + this._fromFrame = !!spec; + + this.spec = spec || {}; + if (options && options.absolutePaths) { + this._options = { toRoot: true }; + } + + /** + * The generated HTML source code of the element + * @type {String} + */ + // TODO: es-modules_audit + if (axe._audit.noHtml) { + this.source = null; + } else if (this.spec.source !== undefined) { + this.source = this.spec.source; + } else { + this.source = getSource(element); + } + + /** + * The element which this object is based off or the containing frame, used for sorting. + * Excluded in toJSON method. + * @type {HTMLElement} + */ + this._element = element; } DqElement.prototype = { diff --git a/test/core/base/audit.js b/test/core/base/audit.js index 930532c136..c00f932ac1 100644 --- a/test/core/base/audit.js +++ b/test/core/base/audit.js @@ -1,1366 +1,1384 @@ /* global Promise */ describe('Audit', function() { - 'use strict'; - - var Audit = axe._thisWillBeDeletedDoNotUse.base.Audit; - var Rule = axe._thisWillBeDeletedDoNotUse.base.Rule; - var a, getFlattenedTree; - var isNotCalled = function(err) { - throw err || new Error('Reject should not be called'); - }; - var noop = function() {}; - - var mockChecks = [ - { - id: 'positive1-check1', - evaluate: function() { - return true; - } - }, - { - id: 'positive2-check1', - evaluate: function() { - return true; - } - }, - { - id: 'negative1-check1', - evaluate: function() { - return true; - } - }, - { - id: 'positive3-check1', - evaluate: function() { - return true; - } - } - ]; - - 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'] - } - ]; - - var fixture = document.getElementById('fixture'); - - var origAuditRun; - var origAxeUtilsPreload; - - beforeEach(function() { - a = new Audit(); - mockRules.forEach(function(r) { - a.addRule(r); - }); - mockChecks.forEach(function(c) { - a.addCheck(c); - }); - origAuditRun = a.run; - }); - - afterEach(function() { - fixture.innerHTML = ''; - axe._tree = undefined; - axe._selectCache = undefined; - a.run = origAuditRun; - }); - - it('should be a function', function() { - assert.isFunction(Audit); - }); - - describe('Audit#_constructHelpUrls', function() { - it('should create default help URLS', function() { - var audit = new Audit(); - audit.addRule({ - id: 'target', - matches: 'function () {return "hello";}', - selector: 'bob' - }); - assert.lengthOf(audit.rules, 1); - assert.equal(audit.data.rules.target, undefined); - audit._constructHelpUrls(); - assert.deepEqual(audit.data.rules.target, { - helpUrl: - 'https://dequeuniversity.com/rules/axe/x.y/target?application=axeAPI' - }); - }); - it('should use changed branding', function() { - var audit = new Audit(); - audit.addRule({ - id: 'target', - matches: 'function () {return "hello";}', - selector: 'bob' - }); - assert.lengthOf(audit.rules, 1); - assert.equal(audit.data.rules.target, undefined); - audit.brand = 'thing'; - audit._constructHelpUrls(); - assert.deepEqual(audit.data.rules.target, { - helpUrl: - 'https://dequeuniversity.com/rules/thing/x.y/target?application=axeAPI' - }); - }); - it('should use changed application', function() { - var audit = new Audit(); - audit.addRule({ - id: 'target', - matches: 'function () {return "hello";}', - selector: 'bob' - }); - assert.lengthOf(audit.rules, 1); - assert.equal(audit.data.rules.target, undefined); - audit.application = 'thing'; - audit._constructHelpUrls(); - assert.deepEqual(audit.data.rules.target, { - helpUrl: - 'https://dequeuniversity.com/rules/axe/x.y/target?application=thing' - }); - }); - - it('does not override helpUrls of different products', function() { - var audit = new Audit(); - audit.addRule({ - id: 'target1', - matches: 'function () {return "hello";}', - selector: 'bob', - metadata: { - helpUrl: - 'https://dequeuniversity.com/rules/myproject/x.y/target1?application=axeAPI' - } - }); - audit.addRule({ - id: 'target2', - matches: 'function () {return "hello";}', - selector: 'bob' - }); - - assert.equal( - audit.data.rules.target1.helpUrl, - 'https://dequeuniversity.com/rules/myproject/x.y/target1?application=axeAPI' - ); - assert.isUndefined(audit.data.rules.target2); - - assert.lengthOf(audit.rules, 2); - audit.brand = 'thing'; - audit._constructHelpUrls(); - - assert.equal( - audit.data.rules.target1.helpUrl, - 'https://dequeuniversity.com/rules/myproject/x.y/target1?application=axeAPI' - ); - assert.equal( - audit.data.rules.target2.helpUrl, - 'https://dequeuniversity.com/rules/thing/x.y/target2?application=axeAPI' - ); - }); - it('understands prerelease type version numbers', function() { - var tempVersion = axe.version; - var audit = new Audit(); - audit.addRule({ - id: 'target', - matches: 'function () {return "hello";}', - selector: 'bob' - }); - - axe.version = '3.2.1-alpha.0'; - audit._constructHelpUrls(); - - axe.version = tempVersion; - assert.equal( - audit.data.rules.target.helpUrl, - 'https://dequeuniversity.com/rules/axe/3.2/target?application=axeAPI' - ); - }); - it('sets x.y as version for invalid versions', function() { - var tempVersion = axe.version; - var audit = new Audit(); - audit.addRule({ - id: 'target', - matches: 'function () {return "hello";}', - selector: 'bob' - }); - - axe.version = 'in-3.0-valid'; - audit._constructHelpUrls(); - - axe.version = tempVersion; - assert.equal( - audit.data.rules.target.helpUrl, - 'https://dequeuniversity.com/rules/axe/x.y/target?application=axeAPI' - ); - }); - it('matches major release versions', function() { - var tempVersion = axe.version; - var audit = new Audit(); - audit.addRule({ - id: 'target', - matches: 'function () {return "hello";}', - selector: 'bob' - }); - - axe.version = '1.0.0'; - audit._constructHelpUrls(); - - axe.version = tempVersion; - assert.equal( - audit.data.rules.target.helpUrl, - 'https://dequeuniversity.com/rules/axe/1.0/target?application=axeAPI' - ); - }); - it('sets the lang query if locale has been set', function() { - var audit = new Audit(); - audit.addRule({ - id: 'target', - matches: 'function () {return "hello";}', - selector: 'bob' - }); - audit.applyLocale({ - lang: 'de' - }); - assert.lengthOf(audit.rules, 1); - assert.equal(audit.data.rules.target, undefined); - audit._constructHelpUrls(); - assert.deepEqual(audit.data.rules.target, { - helpUrl: - 'https://dequeuniversity.com/rules/axe/x.y/target?application=axeAPI&lang=de' - }); - }); - }); - - describe('Audit#setBranding', function() { - it('should change the brand', function() { - var audit = new Audit(); - assert.equal(audit.brand, 'axe'); - assert.equal(audit.application, 'axeAPI'); - audit.setBranding({ - brand: 'thing' - }); - assert.equal(audit.brand, 'thing'); - assert.equal(audit.application, 'axeAPI'); - }); - it('should change the application', function() { - var audit = new Audit(); - assert.equal(audit.brand, 'axe'); - assert.equal(audit.application, 'axeAPI'); - audit.setBranding({ - application: 'thing' - }); - assert.equal(audit.brand, 'axe'); - assert.equal(audit.application, 'thing'); - }); - it('should call _constructHelpUrls', function() { - var audit = new Audit(); - audit.addRule({ - id: 'target', - matches: 'function () {return "hello";}', - selector: 'bob' - }); - assert.lengthOf(audit.rules, 1); - assert.equal(audit.data.rules.target, undefined); - audit.setBranding({ - application: 'thing' - }); - assert.deepEqual(audit.data.rules.target, { - helpUrl: - 'https://dequeuniversity.com/rules/axe/x.y/target?application=thing' - }); - }); - it('should call _constructHelpUrls even when nothing changed', function() { - var audit = new Audit(); - audit.addRule({ - id: 'target', - matches: 'function () {return "hello";}', - selector: 'bob' - }); - assert.lengthOf(audit.rules, 1); - assert.equal(audit.data.rules.target, undefined); - audit.setBranding(undefined); - assert.deepEqual(audit.data.rules.target, { - helpUrl: - 'https://dequeuniversity.com/rules/axe/x.y/target?application=axeAPI' - }); - }); - it('should not replace custom set branding', function() { - var audit = new Audit(); - audit.addRule({ - id: 'target', - matches: 'function () {return "hello";}', - selector: 'bob', - metadata: { - helpUrl: - 'https://dequeuniversity.com/rules/customer-x/x.y/target?application=axeAPI' - } - }); - audit.setBranding({ - application: 'thing', - brand: 'other' - }); - assert.equal( - audit.data.rules.target.helpUrl, - 'https://dequeuniversity.com/rules/customer-x/x.y/target?application=axeAPI' - ); - }); - }); - - describe('Audit#addRule', function() { - it('should override existing rule', function() { - var audit = new Audit(); - audit.addRule({ - id: 'target', - matches: 'function () {return "hello";}', - selector: 'bob' - }); - assert.lengthOf(audit.rules, 1); - assert.equal(audit.rules[0].selector, 'bob'); - assert.equal(audit.rules[0].matches(), 'hello'); - - audit.addRule({ - id: 'target', - selector: 'fred' - }); - - assert.lengthOf(audit.rules, 1); - assert.equal(audit.rules[0].selector, 'fred'); - assert.equal(audit.rules[0].matches(), 'hello'); - }); - it('should otherwise push new rule', function() { - var audit = new Audit(); - audit.addRule({ - id: 'target', - selector: 'bob' - }); - assert.lengthOf(audit.rules, 1); - assert.equal(audit.rules[0].id, 'target'); - assert.equal(audit.rules[0].selector, 'bob'); - - audit.addRule({ - id: 'target2', - selector: 'fred' - }); - - assert.lengthOf(audit.rules, 2); - assert.equal(audit.rules[1].id, 'target2'); - assert.equal(audit.rules[1].selector, 'fred'); - }); - }); - - describe('Audit#resetRulesAndChecks', function() { - it('should override newly created check', function() { - var audit = new Audit(); - assert.equal(audit.checks.target, undefined); - audit.addCheck({ - id: 'target', - options: { value: 'jane' } - }); - assert.ok(audit.checks.target); - assert.deepEqual(audit.checks.target.options, { value: 'jane' }); - audit.resetRulesAndChecks(); - assert.equal(audit.checks.target, undefined); - }); - it('should reset locale', function() { - var audit = new Audit(); - assert.equal(audit.lang, 'en'); - audit.applyLocale({ - lang: 'de' - }); - assert.equal(audit.lang, 'de'); - audit.resetRulesAndChecks(); - assert.equal(audit.lang, 'en'); - }); - it('should reset brand', function() { - var audit = new Audit(); - assert.equal(audit.brand, 'axe'); - audit.setBranding({ - brand: 'test' - }); - assert.equal(audit.brand, 'test'); - audit.resetRulesAndChecks(); - assert.equal(audit.brand, 'axe'); - }); - it('should reset brand application', function() { - var audit = new Audit(); - assert.equal(audit.application, 'axeAPI'); - audit.setBranding({ - application: 'test' - }); - assert.equal(audit.application, 'test'); - audit.resetRulesAndChecks(); - assert.equal(audit.application, 'axeAPI'); - }); - it('should reset brand tagExlcude', function() { - axe._load({}); - assert.deepEqual(axe._audit.tagExclude, ['experimental']); - axe.configure({ - tagExclude: ['ninjas'] - }); - axe._audit.resetRulesAndChecks(); - assert.deepEqual(axe._audit.tagExclude, ['experimental']); - }); - }); - - describe('Audit#addCheck', function() { - it('should create a new check', function() { - var audit = new Audit(); - assert.equal(audit.checks.target, undefined); - audit.addCheck({ - id: 'target', - options: { value: 'jane' } - }); - assert.ok(audit.checks.target); - assert.deepEqual(audit.checks.target.options, { value: 'jane' }); - }); - it('should configure the metadata, if passed', function() { - var audit = new Audit(); - assert.equal(audit.checks.target, undefined); - audit.addCheck({ - id: 'target', - metadata: { guy: 'bob' } - }); - assert.ok(audit.checks.target); - assert.equal(audit.data.checks.target.guy, 'bob'); - }); - it('should reconfigure existing check', function() { - var audit = new Audit(); - var myTest = function() {}; - audit.addCheck({ - id: 'target', - evaluate: myTest, - options: { value: 'jane' } - }); - - assert.deepEqual(audit.checks.target.options, { value: 'jane' }); - - audit.addCheck({ - id: 'target', - options: { value: 'fred' } - }); - - assert.equal(audit.checks.target.evaluate, myTest); - assert.deepEqual(audit.checks.target.options, { value: 'fred' }); - }); - it('should not turn messages into a function', function() { - var audit = new Audit(); - var spec = { - id: 'target', - evaluate: 'function () { return "blah";}', - metadata: { - messages: { - fail: 'it failed' - } - } - }; - audit.addCheck(spec); - - assert.equal(typeof audit.checks.target.evaluate, 'function'); - assert.equal(typeof audit.data.checks.target.messages.fail, 'string'); - assert.equal(audit.data.checks.target.messages.fail, 'it failed'); - }); - - it('should turn function strings into a function', function() { - var audit = new Audit(); - var spec = { - id: 'target', - evaluate: 'function () { return "blah";}', - metadata: { - messages: { - fail: 'function () {return "it failed";}' - } - } - }; - audit.addCheck(spec); - - assert.equal(typeof audit.checks.target.evaluate, 'function'); - assert.equal(typeof audit.data.checks.target.messages.fail, 'function'); - assert.equal(audit.data.checks.target.messages.fail(), 'it failed'); - }); - }); - - describe('Audit#run', function() { - it('should run all the rules', function(done) { - fixture.innerHTML = - '' + - '
bananas
' + - '' + - 'FAIL ME'; - - a.run( - { include: [axe.utils.getFlattenedTree(fixture)[0]] }, - {}, - function(results) { - var expected = [ - { - id: 'positive1', - result: 'inapplicable', - pageLevel: false, - impact: null, - nodes: '...other tests cover this...' - }, - { - id: 'positive2', - result: 'inapplicable', - pageLevel: false, - impact: null, - nodes: '...other tests cover this...' - }, - { - id: 'negative1', - result: 'inapplicable', - pageLevel: false, - impact: null, - nodes: '...other tests cover this...' - }, - { - id: 'positive3', - result: 'inapplicable', - pageLevel: false, - impact: null, - nodes: '...other tests cover this...' - } - ]; - - var out = results[0].nodes[0].node.source; - results.forEach(function(res) { - // attribute order is a pain in the lower back in IE, so we're not - // comparing nodes. Check.run and Rule.run do this. - res.nodes = '...other tests cover this...'; - }); - - assert.deepEqual(JSON.parse(JSON.stringify(results)), expected); - assert.match( - out, - /^/ - ); - done(); - }, - isNotCalled - ); - }); - - it('should not run rules disabled by the options', function(done) { - a.run( - { include: [document] }, - { - rules: { - positive3: { - enabled: false - } - } - }, - function(results) { - assert.equal(results.length, 3); - done(); - }, - isNotCalled - ); - }); - - it('should ensure audit.run recieves preload options', function(done) { - fixture.innerHTML = ''; - - var audit = new Audit(); - audit.addRule({ - id: 'preload1', - selector: '*' - }); - audit.run = function(context, options, resolve, reject) { - var randomRule = this.rules[0]; - randomRule.run( - context, - options, - function(ruleResult) { - ruleResult.OPTIONS_PASSED = options; - resolve([ruleResult]); - }, - reject - ); - }; - - var preloadOptions = { - preload: { - assets: ['cssom'] - } - }; - audit.run( - { include: [axe.utils.getFlattenedTree(fixture)[0]] }, - { - preload: preloadOptions - }, - function(res) { - assert.isDefined(res); - - assert.lengthOf(res, 1); - assert.property(res[0], 'OPTIONS_PASSED'); - - var optionsPassed = res[0].OPTIONS_PASSED; - assert.property(optionsPassed, 'preload'); - assert.deepEqual(optionsPassed.preload, preloadOptions); - - // ensure cache is cleared - assert.isTrue(typeof axe._selectCache === 'undefined'); - - done(); - }, - noop - ); - }); - - it.skip('should run rules (that do not need preload) and preload assets simultaneously', function(done) { - /** - * Note: - * overriding and resolving both check and preload with a delay, - * but the invoked timestamp should ensure that they were invoked almost immediately - */ - - fixture.innerHTML = '
'; - - var runStartTime = new Date(); - var preloadInvokedTime = new Date(); - var noPreloadCheckedInvokedTime = new Date(); - var noPreloadRuleCheckEvaluateInvoked = false; - var preloadOverrideInvoked = false; - - // override preload method - axe.utils.preload = function(options) { - preloadInvokedTime = new Date(); - preloadOverrideInvoked = true; - - return new Promise(function(res, rej) { - setTimeout(function() { - res(true); - }, 2000); - }); - }; - - var audit = new Audit(); - // add a rule and check that does not need preload - audit.addRule({ - id: 'no-preload', - selector: 'div#div1', - any: ['no-preload-check'], - preload: false - }); - audit.addCheck({ - id: 'no-preload-check', - evaluate: function(node, options, vNode, context) { - noPreloadCheckedInvokedTime = new Date(); - noPreloadRuleCheckEvaluateInvoked = true; - var ready = this.async(); - setTimeout(function() { - ready(true); - }, 1000); - } - }); - - // add a rule which needs preload - audit.addRule({ - id: 'yes-preload', - selector: 'div#div2', - preload: true - }); - - var preloadOptions = { - preload: { - assets: ['cssom'] - } - }; - - var allowedDiff = 50; - - audit.run( - { include: [axe.utils.getFlattenedTree(fixture)[0]] }, - { - preload: preloadOptions - }, - function(results) { - assert.isDefined(results); - // assert that check was invoked for rule(s) - assert.isTrue(noPreloadRuleCheckEvaluateInvoked); - // assert preload was invoked - assert.isTrue(preloadOverrideInvoked); - // assert that time diff(s) - // assert that run check invoked immediately - // choosing 5ms as an arbitary number - assert.isBelow( - noPreloadCheckedInvokedTime - runStartTime, - allowedDiff - ); - // assert that preload invoked immediately - assert.isBelow(preloadInvokedTime - runStartTime, allowedDiff); - // ensure cache is clear - assert.isTrue(typeof axe._selectCache === 'undefined'); - // done - done(); - }, - noop - ); - }); - - it.skip('should pass assets from preload to rule check that needs assets as context', function(done) { - fixture.innerHTML = '
'; - - var yesPreloadRuleCheckEvaluateInvoked = false; - var preloadOverrideInvoked = false; - - var preloadData = { - data: 'you got it!' - }; - // override preload method - axe.utils.preload = function(options) { - preloadOverrideInvoked = true; - return Promise.resolve({ - cssom: preloadData - }); - }; - - var audit = new Audit(); - // add a rule and check that does not need preload - audit.addRule({ - id: 'no-preload', - selector: 'div#div1', - preload: false - }); - // add a rule which needs preload - audit.addRule({ - id: 'yes-preload', - selector: 'div#div2', - preload: true, - any: ['yes-preload-check'] - }); - audit.addCheck({ - id: 'yes-preload-check', - evaluate: function(node, options, vNode, context) { - yesPreloadRuleCheckEvaluateInvoked = true; - this.data(context); - return true; - } - }); - - var preloadOptions = { - preload: { - assets: ['cssom'] - } - }; - audit.run( - { include: [axe.utils.getFlattenedTree(fixture)[0]] }, - { - preload: preloadOptions - }, - function(results) { - assert.isDefined(results); - // assert that check was invoked for rule(s) - assert.isTrue(yesPreloadRuleCheckEvaluateInvoked); - // assert preload was invoked - assert.isTrue(preloadOverrideInvoked); - - // assert preload data that was passed to check - var ruleResult = results.filter(function(r) { - return (r.id = 'yes-preload' && r.nodes.length > 0); - })[0]; - var checkResult = ruleResult.nodes[0].any[0]; - assert.isDefined(checkResult.data); - assert.property(checkResult.data, 'cssom'); - assert.deepEqual(checkResult.data.cssom, preloadData); - // ensure cache is clear - assert.isTrue(typeof axe._selectCache === 'undefined'); - // done - done(); - }, - noop - ); - }); - - it.skip('should continue to run rules and return result when preload is rejected', function(done) { - fixture.innerHTML = '
'; - - var preloadOverrideInvoked = false; - var preloadNeededCheckInvoked = false; - var rejectionMsg = - 'Boom! Things went terribly wrong! (But this was intended in this test)'; - - // override preload method - axe.utils.preload = function(options) { - preloadOverrideInvoked = true; - return Promise.reject(rejectionMsg); - }; - - var audit = new Audit(); - // add a rule and check that does not need preload - audit.addRule({ - id: 'no-preload', - selector: 'div#div1', - preload: false - }); - // add a rule which needs preload - audit.addRule({ - id: 'yes-preload', - selector: 'div#div2', - preload: true, - any: ['yes-preload-check'] - }); - audit.addCheck({ - id: 'yes-preload-check', - evaluate: function(node, options, vNode, context) { - preloadNeededCheckInvoked = true; - this.data(context); - return true; - } - }); - - var preloadOptions = { - preload: { - assets: ['cssom'] - } - }; - audit.run( - { include: [axe.utils.getFlattenedTree(fixture)[0]] }, - { - preload: preloadOptions - }, - function(results) { - assert.isDefined(results); - // assert preload was invoked - assert.isTrue(preloadOverrideInvoked); - - // assert that both rules ran, although preload failed - assert.lengthOf(results, 2); - - // assert that because preload failed - // cssom was not populated on context of repective check - assert.isTrue(preloadNeededCheckInvoked); - var ruleResult = results.filter(function(r) { - return (r.id = 'yes-preload' && r.nodes.length > 0); - })[0]; - var checkResult = ruleResult.nodes[0].any[0]; - assert.isDefined(checkResult.data); - assert.notProperty(checkResult.data, 'cssom'); - // done - done(); - }, - noop - ); - }); - - it('should continue to run rules and return result when axios time(s)out and rejects preload', function(done) { - fixture.innerHTML = '
'; - - // there is no stubbing here, - // the actual axios call is invoked, and timedout immediately as timeout is set to 0.1 - - var preloadNeededCheckInvoked = false; - var audit = new Audit(); - // add a rule and check that does not need preload - audit.addRule({ - id: 'no-preload', - selector: 'div#div1', - preload: false - }); - // add a rule which needs preload - audit.addRule({ - id: 'yes-preload', - selector: 'div#div2', - preload: true, - any: ['yes-preload-check'] - }); - audit.addCheck({ - id: 'yes-preload-check', - evaluate: function(node, options, vNode, context) { - preloadNeededCheckInvoked = true; - this.data(context); - return true; - } - }); - - var preloadOptions = { - preload: { - assets: ['cssom'], - timeout: 0.1 - } - }; - audit.run( - { include: [axe.utils.getFlattenedTree(fixture)[0]] }, - { - preload: preloadOptions - }, - function(results) { - assert.isDefined(results); - // assert that both rules ran, although preload failed - assert.lengthOf(results, 2); - - // assert that because preload failed - // cssom was not populated on context of repective check - assert.isTrue(preloadNeededCheckInvoked); - var ruleResult = results.filter(function(r) { - return (r.id = 'yes-preload' && r.nodes.length > 0); - })[0]; - var checkResult = ruleResult.nodes[0].any[0]; - assert.isDefined(checkResult.data); - assert.notProperty(checkResult.data, 'cssom'); - // done - done(); - }, - noop - ); - }); - - it.skip('should assign an empty array to axe._selectCache', function(done) { - var saved = axe.utils.ruleShouldRun; - axe.utils.ruleShouldRun = function() { - assert.equal(axe._selectCache.length, 0); - return false; - }; - a.run( - { include: [document] }, - {}, - function() { - axe.utils.ruleShouldRun = saved; - done(); - }, - isNotCalled - ); - }); - - it('should clear axe._selectCache', function(done) { - a.run( - { include: [document] }, - { - rules: {} - }, - function() { - assert.isTrue(typeof axe._selectCache === 'undefined'); - done(); - }, - isNotCalled - ); - }); - - it('should not run rules disabled by the configuration', function(done) { - var a = new Audit(); - var success = true; - a.rules.push( - new Rule({ - id: 'positive1', - selector: '*', - enabled: false, - any: [ - { - id: 'positive1-check1', - evaluate: function() { - success = false; - } - } - ] - }) - ); - a.run( - { include: [document] }, - {}, - function() { - assert.ok(success); - done(); - }, - isNotCalled - ); - }); - - it("should call the rule's run function", function(done) { - var targetRule = mockRules[mockRules.length - 1], - rule = axe.utils.findBy(a.rules, 'id', targetRule.id), - called = false, - orig; - - fixture.innerHTML = 'link'; - orig = rule.run; - rule.run = function(node, options, callback) { - called = true; - callback({}); - }; - a.run( - { include: [document] }, - {}, - function() { - assert.isTrue(called); - rule.run = orig; - done(); - }, - isNotCalled - ); - }); - - it('should pass the option to the run function', function(done) { - var targetRule = mockRules[mockRules.length - 1], - rule = axe.utils.findBy(a.rules, 'id', targetRule.id), - passed = false, - orig, - options; - - fixture.innerHTML = 'link'; - orig = rule.run; - rule.run = function(node, o, callback) { - assert.deepEqual(o, options); - passed = true; - callback({}); - }; - options = { rules: {} }; - (options.rules[targetRule.id] = {}).data = 'monkeys'; - a.run( - { include: [document] }, - options, - function() { - assert.ok(passed); - rule.run = orig; - done(); - }, - isNotCalled - ); - }); - - it('should skip pageLevel rules if context is not set to entire page', function() { - var audit = new Audit(); - - audit.rules.push( - new Rule({ - pageLevel: true, - enabled: true, - evaluate: function() { - assert.ok(false, 'Should not run'); - } - }) - ); - - audit.run( - { include: [document.body], page: false }, - {}, - function(results) { - assert.deepEqual(results, []); - }, - isNotCalled - ); - }); - - it('catches errors and passes them as a cantTell result', function(done) { - var err = new Error('Launch the super sheep!'); - a.addRule({ - id: 'throw1', - selector: '*', - any: [ - { - id: 'throw1-check1' - } - ] - }); - a.addCheck({ - id: 'throw1-check1', - evaluate: function() { - throw err; - } - }); - axe._tree = axe.utils.getFlattenedTree(fixture); - axe._selectorData = axe.utils.getSelectorData(axe._tree); - a.run( - { include: [axe._tree[0]] }, - { - runOnly: { - type: 'rule', - values: ['throw1'] - } - }, - function(results) { - assert.lengthOf(results, 1); - assert.equal(results[0].result, 'cantTell'); - assert.equal(results[0].message, err.message); - assert.equal(results[0].stack, err.stack); - assert.equal(results[0].error, err); - done(); - }, - isNotCalled - ); - }); - - it('should not halt if errors occur', function(done) { - a.addRule({ - id: 'throw1', - selector: '*', - any: [ - { - id: 'throw1-check1' - } - ] - }); - a.addCheck({ - id: 'throw1-check1', - evaluate: function() { - throw new Error('Launch the super sheep!'); - } - }); - a.run( - { include: [axe.utils.getFlattenedTree(fixture)[0]] }, - { - runOnly: { - type: 'rule', - values: ['throw1', 'positive1'] - } - }, - function() { - done(); - }, - isNotCalled - ); - }); - - it('should run audit.normalizeOptions to ensure valid input', function() { - fixture.innerHTML = - '' + - '
bananas
' + - '' + - 'FAIL ME'; - var checked = 'options not validated'; - - a.normalizeOptions = function() { - checked = 'options validated'; - }; - - a.run({ include: [fixture] }, {}, noop, isNotCalled); - assert.equal(checked, 'options validated'); - }); - - it('should halt if an error occurs when debug is set', function(done) { - a.addRule({ - id: 'throw1', - selector: '*', - any: [ - { - id: 'throw1-check1' - } - ] - }); - a.addCheck({ - id: 'throw1-check1', - evaluate: function() { - throw new Error('Launch the super sheep!'); - } - }); - a.run( - { include: [axe.utils.getFlattenedTree(fixture)[0]] }, - { - debug: true, - runOnly: { - type: 'rule', - values: ['throw1'] - } - }, - noop, - function(err) { - assert.equal(err.message, 'Launch the super sheep!'); - done(); - } - ); - }); - }); - - describe('Audit#after', function() { - it('should run Rule#after on any rule whose result is passed in', function() { - /*eslint no-unused-vars:0*/ - var audit = new Audit(); - var success = false; - var options = [{ id: 'hehe', enabled: true, monkeys: 'bananas' }]; - var results = [ - { - id: 'hehe', - monkeys: 'bananas' - } - ]; - audit.rules.push( - new Rule({ - id: 'hehe', - pageLevel: false, - enabled: false - }) - ); - - audit.rules[0].after = function(res, opts) { - assert.equal(res, results[0]); - assert.deepEqual(opts, options); - success = true; - }; - - audit.after(results, options); - }); - }); - - describe('Audit#normalizeOptions', function() { - it('returns the options object when it is valid', function() { - var opt = { - runOnly: { - type: 'rule', - values: ['positive1', 'positive2'] - }, - rules: { - negative1: { enabled: false } - } - }; - 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('allows runOnly as an array as an alternative to type: rule', function() { - var opt = { runOnly: ['positive1', 'negative1'] }; - var out = a.normalizeOptions(opt); - assert(out.runOnly.type, 'rule'); - assert.deepEqual(out.runOnly.values, ['positive1', 'negative1']); - }); - - it('throws an error if runOnly contains both rules and tags', function() { - assert.throws(function() { - a.normalizeOptions({ - runOnly: ['positive', 'negative1'] - }); - }); - }); - - it('defaults runOnly to type: tag', function() { - var opt = { runOnly: ['fakeTag'] }; - var out = a.normalizeOptions(opt); - assert(out.runOnly.type, 'tag'); - assert.deepEqual(out.runOnly.values, ['fakeTag']); - }); - - 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.normalizeOptions({ - runOnly: { - type: 'rule', - values: ['frakeRule'] - } - }); - }); - }); - - it("doesn't throw an error when option.runOnly has an unknown tag", function() { - assert.doesNotThrow(function() { - a.normalizeOptions({ - runOnly: { - type: 'tags', - values: ['fakeTag'] - } - }); - }); - }); - - it('throws an error when option.rules has an unknown rule', function() { - assert.throws(function() { - a.normalizeOptions({ - rules: { - fakeRule: { enabled: false } - } - }); - }); - }); - }); + 'use strict'; + + var Audit = axe._thisWillBeDeletedDoNotUse.base.Audit; + var Rule = axe._thisWillBeDeletedDoNotUse.base.Rule; + var a, getFlattenedTree; + var isNotCalled = function(err) { + throw err || new Error('Reject should not be called'); + }; + var noop = function() {}; + + var mockChecks = [ + { + id: 'positive1-check1', + evaluate: function() { + return true; + } + }, + { + id: 'positive2-check1', + evaluate: function() { + return true; + } + }, + { + id: 'negative1-check1', + evaluate: function() { + return true; + } + }, + { + id: 'positive3-check1', + evaluate: function() { + return true; + } + } + ]; + + 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'] + } + ]; + + var fixture = document.getElementById('fixture'); + + var origAuditRun; + var origAxeUtilsPreload; + + beforeEach(function() { + a = new Audit(); + mockRules.forEach(function(r) { + a.addRule(r); + }); + mockChecks.forEach(function(c) { + a.addCheck(c); + }); + origAuditRun = a.run; + }); + + afterEach(function() { + fixture.innerHTML = ''; + axe._tree = undefined; + axe._selectCache = undefined; + a.run = origAuditRun; + }); + + it('should be a function', function() { + assert.isFunction(Audit); + }); + + describe('defaults', function() { + it('should set noHtml', function() { + var audit = new Audit(); + assert.isFalse(audit.noHtml); + }); + }); + + describe('Audit#_constructHelpUrls', function() { + it('should create default help URLS', function() { + var audit = new Audit(); + audit.addRule({ + id: 'target', + matches: 'function () {return "hello";}', + selector: 'bob' + }); + assert.lengthOf(audit.rules, 1); + assert.equal(audit.data.rules.target, undefined); + audit._constructHelpUrls(); + assert.deepEqual(audit.data.rules.target, { + helpUrl: + 'https://dequeuniversity.com/rules/axe/x.y/target?application=axeAPI' + }); + }); + it('should use changed branding', function() { + var audit = new Audit(); + audit.addRule({ + id: 'target', + matches: 'function () {return "hello";}', + selector: 'bob' + }); + assert.lengthOf(audit.rules, 1); + assert.equal(audit.data.rules.target, undefined); + audit.brand = 'thing'; + audit._constructHelpUrls(); + assert.deepEqual(audit.data.rules.target, { + helpUrl: + 'https://dequeuniversity.com/rules/thing/x.y/target?application=axeAPI' + }); + }); + it('should use changed application', function() { + var audit = new Audit(); + audit.addRule({ + id: 'target', + matches: 'function () {return "hello";}', + selector: 'bob' + }); + assert.lengthOf(audit.rules, 1); + assert.equal(audit.data.rules.target, undefined); + audit.application = 'thing'; + audit._constructHelpUrls(); + assert.deepEqual(audit.data.rules.target, { + helpUrl: + 'https://dequeuniversity.com/rules/axe/x.y/target?application=thing' + }); + }); + + it('does not override helpUrls of different products', function() { + var audit = new Audit(); + audit.addRule({ + id: 'target1', + matches: 'function () {return "hello";}', + selector: 'bob', + metadata: { + helpUrl: + 'https://dequeuniversity.com/rules/myproject/x.y/target1?application=axeAPI' + } + }); + audit.addRule({ + id: 'target2', + matches: 'function () {return "hello";}', + selector: 'bob' + }); + + assert.equal( + audit.data.rules.target1.helpUrl, + 'https://dequeuniversity.com/rules/myproject/x.y/target1?application=axeAPI' + ); + assert.isUndefined(audit.data.rules.target2); + + assert.lengthOf(audit.rules, 2); + audit.brand = 'thing'; + audit._constructHelpUrls(); + + assert.equal( + audit.data.rules.target1.helpUrl, + 'https://dequeuniversity.com/rules/myproject/x.y/target1?application=axeAPI' + ); + assert.equal( + audit.data.rules.target2.helpUrl, + 'https://dequeuniversity.com/rules/thing/x.y/target2?application=axeAPI' + ); + }); + it('understands prerelease type version numbers', function() { + var tempVersion = axe.version; + var audit = new Audit(); + audit.addRule({ + id: 'target', + matches: 'function () {return "hello";}', + selector: 'bob' + }); + + axe.version = '3.2.1-alpha.0'; + audit._constructHelpUrls(); + + axe.version = tempVersion; + assert.equal( + audit.data.rules.target.helpUrl, + 'https://dequeuniversity.com/rules/axe/3.2/target?application=axeAPI' + ); + }); + it('sets x.y as version for invalid versions', function() { + var tempVersion = axe.version; + var audit = new Audit(); + audit.addRule({ + id: 'target', + matches: 'function () {return "hello";}', + selector: 'bob' + }); + + axe.version = 'in-3.0-valid'; + audit._constructHelpUrls(); + + axe.version = tempVersion; + assert.equal( + audit.data.rules.target.helpUrl, + 'https://dequeuniversity.com/rules/axe/x.y/target?application=axeAPI' + ); + }); + it('matches major release versions', function() { + var tempVersion = axe.version; + var audit = new Audit(); + audit.addRule({ + id: 'target', + matches: 'function () {return "hello";}', + selector: 'bob' + }); + + axe.version = '1.0.0'; + audit._constructHelpUrls(); + + axe.version = tempVersion; + assert.equal( + audit.data.rules.target.helpUrl, + 'https://dequeuniversity.com/rules/axe/1.0/target?application=axeAPI' + ); + }); + it('sets the lang query if locale has been set', function() { + var audit = new Audit(); + audit.addRule({ + id: 'target', + matches: 'function () {return "hello";}', + selector: 'bob' + }); + audit.applyLocale({ + lang: 'de' + }); + assert.lengthOf(audit.rules, 1); + assert.equal(audit.data.rules.target, undefined); + audit._constructHelpUrls(); + assert.deepEqual(audit.data.rules.target, { + helpUrl: + 'https://dequeuniversity.com/rules/axe/x.y/target?application=axeAPI&lang=de' + }); + }); + }); + + describe('Audit#setBranding', function() { + it('should change the brand', function() { + var audit = new Audit(); + assert.equal(audit.brand, 'axe'); + assert.equal(audit.application, 'axeAPI'); + audit.setBranding({ + brand: 'thing' + }); + assert.equal(audit.brand, 'thing'); + assert.equal(audit.application, 'axeAPI'); + }); + it('should change the application', function() { + var audit = new Audit(); + assert.equal(audit.brand, 'axe'); + assert.equal(audit.application, 'axeAPI'); + audit.setBranding({ + application: 'thing' + }); + assert.equal(audit.brand, 'axe'); + assert.equal(audit.application, 'thing'); + }); + it('should call _constructHelpUrls', function() { + var audit = new Audit(); + audit.addRule({ + id: 'target', + matches: 'function () {return "hello";}', + selector: 'bob' + }); + assert.lengthOf(audit.rules, 1); + assert.equal(audit.data.rules.target, undefined); + audit.setBranding({ + application: 'thing' + }); + assert.deepEqual(audit.data.rules.target, { + helpUrl: + 'https://dequeuniversity.com/rules/axe/x.y/target?application=thing' + }); + }); + it('should call _constructHelpUrls even when nothing changed', function() { + var audit = new Audit(); + audit.addRule({ + id: 'target', + matches: 'function () {return "hello";}', + selector: 'bob' + }); + assert.lengthOf(audit.rules, 1); + assert.equal(audit.data.rules.target, undefined); + audit.setBranding(undefined); + assert.deepEqual(audit.data.rules.target, { + helpUrl: + 'https://dequeuniversity.com/rules/axe/x.y/target?application=axeAPI' + }); + }); + it('should not replace custom set branding', function() { + var audit = new Audit(); + audit.addRule({ + id: 'target', + matches: 'function () {return "hello";}', + selector: 'bob', + metadata: { + helpUrl: + 'https://dequeuniversity.com/rules/customer-x/x.y/target?application=axeAPI' + } + }); + audit.setBranding({ + application: 'thing', + brand: 'other' + }); + assert.equal( + audit.data.rules.target.helpUrl, + 'https://dequeuniversity.com/rules/customer-x/x.y/target?application=axeAPI' + ); + }); + }); + + describe('Audit#addRule', function() { + it('should override existing rule', function() { + var audit = new Audit(); + audit.addRule({ + id: 'target', + matches: 'function () {return "hello";}', + selector: 'bob' + }); + assert.lengthOf(audit.rules, 1); + assert.equal(audit.rules[0].selector, 'bob'); + assert.equal(audit.rules[0].matches(), 'hello'); + + audit.addRule({ + id: 'target', + selector: 'fred' + }); + + assert.lengthOf(audit.rules, 1); + assert.equal(audit.rules[0].selector, 'fred'); + assert.equal(audit.rules[0].matches(), 'hello'); + }); + it('should otherwise push new rule', function() { + var audit = new Audit(); + audit.addRule({ + id: 'target', + selector: 'bob' + }); + assert.lengthOf(audit.rules, 1); + assert.equal(audit.rules[0].id, 'target'); + assert.equal(audit.rules[0].selector, 'bob'); + + audit.addRule({ + id: 'target2', + selector: 'fred' + }); + + assert.lengthOf(audit.rules, 2); + assert.equal(audit.rules[1].id, 'target2'); + assert.equal(audit.rules[1].selector, 'fred'); + }); + }); + + describe('Audit#resetRulesAndChecks', function() { + it('should override newly created check', function() { + var audit = new Audit(); + assert.equal(audit.checks.target, undefined); + audit.addCheck({ + id: 'target', + options: { value: 'jane' } + }); + assert.ok(audit.checks.target); + assert.deepEqual(audit.checks.target.options, { value: 'jane' }); + audit.resetRulesAndChecks(); + assert.equal(audit.checks.target, undefined); + }); + it('should reset locale', function() { + var audit = new Audit(); + assert.equal(audit.lang, 'en'); + audit.applyLocale({ + lang: 'de' + }); + assert.equal(audit.lang, 'de'); + audit.resetRulesAndChecks(); + assert.equal(audit.lang, 'en'); + }); + it('should reset brand', function() { + var audit = new Audit(); + assert.equal(audit.brand, 'axe'); + audit.setBranding({ + brand: 'test' + }); + assert.equal(audit.brand, 'test'); + audit.resetRulesAndChecks(); + assert.equal(audit.brand, 'axe'); + }); + it('should reset brand application', function() { + var audit = new Audit(); + assert.equal(audit.application, 'axeAPI'); + audit.setBranding({ + application: 'test' + }); + assert.equal(audit.application, 'test'); + audit.resetRulesAndChecks(); + assert.equal(audit.application, 'axeAPI'); + }); + it('should reset brand tagExlcude', function() { + axe._load({}); + assert.deepEqual(axe._audit.tagExclude, ['experimental']); + axe.configure({ + tagExclude: ['ninjas'] + }); + axe._audit.resetRulesAndChecks(); + assert.deepEqual(axe._audit.tagExclude, ['experimental']); + }); + + it('should reset noHtml', function() { + var audit = new Audit(); + audit.noHtml = true; + audit.resetRulesAndChecks(); + assert.isFalse(audit.noHtml); + }); + }); + + describe('Audit#addCheck', function() { + it('should create a new check', function() { + var audit = new Audit(); + assert.equal(audit.checks.target, undefined); + audit.addCheck({ + id: 'target', + options: { value: 'jane' } + }); + assert.ok(audit.checks.target); + assert.deepEqual(audit.checks.target.options, { value: 'jane' }); + }); + it('should configure the metadata, if passed', function() { + var audit = new Audit(); + assert.equal(audit.checks.target, undefined); + audit.addCheck({ + id: 'target', + metadata: { guy: 'bob' } + }); + assert.ok(audit.checks.target); + assert.equal(audit.data.checks.target.guy, 'bob'); + }); + it('should reconfigure existing check', function() { + var audit = new Audit(); + var myTest = function() {}; + audit.addCheck({ + id: 'target', + evaluate: myTest, + options: { value: 'jane' } + }); + + assert.deepEqual(audit.checks.target.options, { value: 'jane' }); + + audit.addCheck({ + id: 'target', + options: { value: 'fred' } + }); + + assert.equal(audit.checks.target.evaluate, myTest); + assert.deepEqual(audit.checks.target.options, { value: 'fred' }); + }); + it('should not turn messages into a function', function() { + var audit = new Audit(); + var spec = { + id: 'target', + evaluate: 'function () { return "blah";}', + metadata: { + messages: { + fail: 'it failed' + } + } + }; + audit.addCheck(spec); + + assert.equal(typeof audit.checks.target.evaluate, 'function'); + assert.equal(typeof audit.data.checks.target.messages.fail, 'string'); + assert.equal(audit.data.checks.target.messages.fail, 'it failed'); + }); + + it('should turn function strings into a function', function() { + var audit = new Audit(); + var spec = { + id: 'target', + evaluate: 'function () { return "blah";}', + metadata: { + messages: { + fail: 'function () {return "it failed";}' + } + } + }; + audit.addCheck(spec); + + assert.equal(typeof audit.checks.target.evaluate, 'function'); + assert.equal(typeof audit.data.checks.target.messages.fail, 'function'); + assert.equal(audit.data.checks.target.messages.fail(), 'it failed'); + }); + }); + + describe('Audit#run', function() { + it('should run all the rules', function(done) { + fixture.innerHTML = + '' + + '
bananas
' + + '' + + 'FAIL ME'; + + a.run( + { include: [axe.utils.getFlattenedTree(fixture)[0]] }, + {}, + function(results) { + var expected = [ + { + id: 'positive1', + result: 'inapplicable', + pageLevel: false, + impact: null, + nodes: '...other tests cover this...' + }, + { + id: 'positive2', + result: 'inapplicable', + pageLevel: false, + impact: null, + nodes: '...other tests cover this...' + }, + { + id: 'negative1', + result: 'inapplicable', + pageLevel: false, + impact: null, + nodes: '...other tests cover this...' + }, + { + id: 'positive3', + result: 'inapplicable', + pageLevel: false, + impact: null, + nodes: '...other tests cover this...' + } + ]; + + var out = results[0].nodes[0].node.source; + results.forEach(function(res) { + // attribute order is a pain in the lower back in IE, so we're not + // comparing nodes. Check.run and Rule.run do this. + res.nodes = '...other tests cover this...'; + }); + + assert.deepEqual(JSON.parse(JSON.stringify(results)), expected); + assert.match( + out, + /^/ + ); + done(); + }, + isNotCalled + ); + }); + + it('should not run rules disabled by the options', function(done) { + a.run( + { include: [document] }, + { + rules: { + positive3: { + enabled: false + } + } + }, + function(results) { + assert.equal(results.length, 3); + done(); + }, + isNotCalled + ); + }); + + it('should ensure audit.run recieves preload options', function(done) { + fixture.innerHTML = ''; + + var audit = new Audit(); + audit.addRule({ + id: 'preload1', + selector: '*' + }); + audit.run = function(context, options, resolve, reject) { + var randomRule = this.rules[0]; + randomRule.run( + context, + options, + function(ruleResult) { + ruleResult.OPTIONS_PASSED = options; + resolve([ruleResult]); + }, + reject + ); + }; + + var preloadOptions = { + preload: { + assets: ['cssom'] + } + }; + audit.run( + { include: [axe.utils.getFlattenedTree(fixture)[0]] }, + { + preload: preloadOptions + }, + function(res) { + assert.isDefined(res); + + assert.lengthOf(res, 1); + assert.property(res[0], 'OPTIONS_PASSED'); + + var optionsPassed = res[0].OPTIONS_PASSED; + assert.property(optionsPassed, 'preload'); + assert.deepEqual(optionsPassed.preload, preloadOptions); + + // ensure cache is cleared + assert.isTrue(typeof axe._selectCache === 'undefined'); + + done(); + }, + noop + ); + }); + + it.skip('should run rules (that do not need preload) and preload assets simultaneously', function(done) { + /** + * Note: + * overriding and resolving both check and preload with a delay, + * but the invoked timestamp should ensure that they were invoked almost immediately + */ + + fixture.innerHTML = '
'; + + var runStartTime = new Date(); + var preloadInvokedTime = new Date(); + var noPreloadCheckedInvokedTime = new Date(); + var noPreloadRuleCheckEvaluateInvoked = false; + var preloadOverrideInvoked = false; + + // override preload method + axe.utils.preload = function(options) { + preloadInvokedTime = new Date(); + preloadOverrideInvoked = true; + + return new Promise(function(res, rej) { + setTimeout(function() { + res(true); + }, 2000); + }); + }; + + var audit = new Audit(); + // add a rule and check that does not need preload + audit.addRule({ + id: 'no-preload', + selector: 'div#div1', + any: ['no-preload-check'], + preload: false + }); + audit.addCheck({ + id: 'no-preload-check', + evaluate: function(node, options, vNode, context) { + noPreloadCheckedInvokedTime = new Date(); + noPreloadRuleCheckEvaluateInvoked = true; + var ready = this.async(); + setTimeout(function() { + ready(true); + }, 1000); + } + }); + + // add a rule which needs preload + audit.addRule({ + id: 'yes-preload', + selector: 'div#div2', + preload: true + }); + + var preloadOptions = { + preload: { + assets: ['cssom'] + } + }; + + var allowedDiff = 50; + + audit.run( + { include: [axe.utils.getFlattenedTree(fixture)[0]] }, + { + preload: preloadOptions + }, + function(results) { + assert.isDefined(results); + // assert that check was invoked for rule(s) + assert.isTrue(noPreloadRuleCheckEvaluateInvoked); + // assert preload was invoked + assert.isTrue(preloadOverrideInvoked); + // assert that time diff(s) + // assert that run check invoked immediately + // choosing 5ms as an arbitary number + assert.isBelow( + noPreloadCheckedInvokedTime - runStartTime, + allowedDiff + ); + // assert that preload invoked immediately + assert.isBelow(preloadInvokedTime - runStartTime, allowedDiff); + // ensure cache is clear + assert.isTrue(typeof axe._selectCache === 'undefined'); + // done + done(); + }, + noop + ); + }); + + it.skip('should pass assets from preload to rule check that needs assets as context', function(done) { + fixture.innerHTML = '
'; + + var yesPreloadRuleCheckEvaluateInvoked = false; + var preloadOverrideInvoked = false; + + var preloadData = { + data: 'you got it!' + }; + // override preload method + axe.utils.preload = function(options) { + preloadOverrideInvoked = true; + return Promise.resolve({ + cssom: preloadData + }); + }; + + var audit = new Audit(); + // add a rule and check that does not need preload + audit.addRule({ + id: 'no-preload', + selector: 'div#div1', + preload: false + }); + // add a rule which needs preload + audit.addRule({ + id: 'yes-preload', + selector: 'div#div2', + preload: true, + any: ['yes-preload-check'] + }); + audit.addCheck({ + id: 'yes-preload-check', + evaluate: function(node, options, vNode, context) { + yesPreloadRuleCheckEvaluateInvoked = true; + this.data(context); + return true; + } + }); + + var preloadOptions = { + preload: { + assets: ['cssom'] + } + }; + audit.run( + { include: [axe.utils.getFlattenedTree(fixture)[0]] }, + { + preload: preloadOptions + }, + function(results) { + assert.isDefined(results); + // assert that check was invoked for rule(s) + assert.isTrue(yesPreloadRuleCheckEvaluateInvoked); + // assert preload was invoked + assert.isTrue(preloadOverrideInvoked); + + // assert preload data that was passed to check + var ruleResult = results.filter(function(r) { + return (r.id = 'yes-preload' && r.nodes.length > 0); + })[0]; + var checkResult = ruleResult.nodes[0].any[0]; + assert.isDefined(checkResult.data); + assert.property(checkResult.data, 'cssom'); + assert.deepEqual(checkResult.data.cssom, preloadData); + // ensure cache is clear + assert.isTrue(typeof axe._selectCache === 'undefined'); + // done + done(); + }, + noop + ); + }); + + it.skip('should continue to run rules and return result when preload is rejected', function(done) { + fixture.innerHTML = '
'; + + var preloadOverrideInvoked = false; + var preloadNeededCheckInvoked = false; + var rejectionMsg = + 'Boom! Things went terribly wrong! (But this was intended in this test)'; + + // override preload method + axe.utils.preload = function(options) { + preloadOverrideInvoked = true; + return Promise.reject(rejectionMsg); + }; + + var audit = new Audit(); + // add a rule and check that does not need preload + audit.addRule({ + id: 'no-preload', + selector: 'div#div1', + preload: false + }); + // add a rule which needs preload + audit.addRule({ + id: 'yes-preload', + selector: 'div#div2', + preload: true, + any: ['yes-preload-check'] + }); + audit.addCheck({ + id: 'yes-preload-check', + evaluate: function(node, options, vNode, context) { + preloadNeededCheckInvoked = true; + this.data(context); + return true; + } + }); + + var preloadOptions = { + preload: { + assets: ['cssom'] + } + }; + audit.run( + { include: [axe.utils.getFlattenedTree(fixture)[0]] }, + { + preload: preloadOptions + }, + function(results) { + assert.isDefined(results); + // assert preload was invoked + assert.isTrue(preloadOverrideInvoked); + + // assert that both rules ran, although preload failed + assert.lengthOf(results, 2); + + // assert that because preload failed + // cssom was not populated on context of repective check + assert.isTrue(preloadNeededCheckInvoked); + var ruleResult = results.filter(function(r) { + return (r.id = 'yes-preload' && r.nodes.length > 0); + })[0]; + var checkResult = ruleResult.nodes[0].any[0]; + assert.isDefined(checkResult.data); + assert.notProperty(checkResult.data, 'cssom'); + // done + done(); + }, + noop + ); + }); + + it('should continue to run rules and return result when axios time(s)out and rejects preload', function(done) { + fixture.innerHTML = '
'; + + // there is no stubbing here, + // the actual axios call is invoked, and timedout immediately as timeout is set to 0.1 + + var preloadNeededCheckInvoked = false; + var audit = new Audit(); + // add a rule and check that does not need preload + audit.addRule({ + id: 'no-preload', + selector: 'div#div1', + preload: false + }); + // add a rule which needs preload + audit.addRule({ + id: 'yes-preload', + selector: 'div#div2', + preload: true, + any: ['yes-preload-check'] + }); + audit.addCheck({ + id: 'yes-preload-check', + evaluate: function(node, options, vNode, context) { + preloadNeededCheckInvoked = true; + this.data(context); + return true; + } + }); + + var preloadOptions = { + preload: { + assets: ['cssom'], + timeout: 0.1 + } + }; + audit.run( + { include: [axe.utils.getFlattenedTree(fixture)[0]] }, + { + preload: preloadOptions + }, + function(results) { + assert.isDefined(results); + // assert that both rules ran, although preload failed + assert.lengthOf(results, 2); + + // assert that because preload failed + // cssom was not populated on context of repective check + assert.isTrue(preloadNeededCheckInvoked); + var ruleResult = results.filter(function(r) { + return (r.id = 'yes-preload' && r.nodes.length > 0); + })[0]; + var checkResult = ruleResult.nodes[0].any[0]; + assert.isDefined(checkResult.data); + assert.notProperty(checkResult.data, 'cssom'); + // done + done(); + }, + noop + ); + }); + + it.skip('should assign an empty array to axe._selectCache', function(done) { + var saved = axe.utils.ruleShouldRun; + axe.utils.ruleShouldRun = function() { + assert.equal(axe._selectCache.length, 0); + return false; + }; + a.run( + { include: [document] }, + {}, + function() { + axe.utils.ruleShouldRun = saved; + done(); + }, + isNotCalled + ); + }); + + it('should clear axe._selectCache', function(done) { + a.run( + { include: [document] }, + { + rules: {} + }, + function() { + assert.isTrue(typeof axe._selectCache === 'undefined'); + done(); + }, + isNotCalled + ); + }); + + it('should not run rules disabled by the configuration', function(done) { + var a = new Audit(); + var success = true; + a.rules.push( + new Rule({ + id: 'positive1', + selector: '*', + enabled: false, + any: [ + { + id: 'positive1-check1', + evaluate: function() { + success = false; + } + } + ] + }) + ); + a.run( + { include: [document] }, + {}, + function() { + assert.ok(success); + done(); + }, + isNotCalled + ); + }); + + it("should call the rule's run function", function(done) { + var targetRule = mockRules[mockRules.length - 1], + rule = axe.utils.findBy(a.rules, 'id', targetRule.id), + called = false, + orig; + + fixture.innerHTML = 'link'; + orig = rule.run; + rule.run = function(node, options, callback) { + called = true; + callback({}); + }; + a.run( + { include: [document] }, + {}, + function() { + assert.isTrue(called); + rule.run = orig; + done(); + }, + isNotCalled + ); + }); + + it('should pass the option to the run function', function(done) { + var targetRule = mockRules[mockRules.length - 1], + rule = axe.utils.findBy(a.rules, 'id', targetRule.id), + passed = false, + orig, + options; + + fixture.innerHTML = 'link'; + orig = rule.run; + rule.run = function(node, o, callback) { + assert.deepEqual(o, options); + passed = true; + callback({}); + }; + options = { rules: {} }; + (options.rules[targetRule.id] = {}).data = 'monkeys'; + a.run( + { include: [document] }, + options, + function() { + assert.ok(passed); + rule.run = orig; + done(); + }, + isNotCalled + ); + }); + + it('should skip pageLevel rules if context is not set to entire page', function() { + var audit = new Audit(); + + audit.rules.push( + new Rule({ + pageLevel: true, + enabled: true, + evaluate: function() { + assert.ok(false, 'Should not run'); + } + }) + ); + + audit.run( + { include: [document.body], page: false }, + {}, + function(results) { + assert.deepEqual(results, []); + }, + isNotCalled + ); + }); + + it('catches errors and passes them as a cantTell result', function(done) { + var err = new Error('Launch the super sheep!'); + a.addRule({ + id: 'throw1', + selector: '*', + any: [ + { + id: 'throw1-check1' + } + ] + }); + a.addCheck({ + id: 'throw1-check1', + evaluate: function() { + throw err; + } + }); + axe._tree = axe.utils.getFlattenedTree(fixture); + axe._selectorData = axe.utils.getSelectorData(axe._tree); + a.run( + { include: [axe._tree[0]] }, + { + runOnly: { + type: 'rule', + values: ['throw1'] + } + }, + function(results) { + assert.lengthOf(results, 1); + assert.equal(results[0].result, 'cantTell'); + assert.equal(results[0].message, err.message); + assert.equal(results[0].stack, err.stack); + assert.equal(results[0].error, err); + done(); + }, + isNotCalled + ); + }); + + it('should not halt if errors occur', function(done) { + a.addRule({ + id: 'throw1', + selector: '*', + any: [ + { + id: 'throw1-check1' + } + ] + }); + a.addCheck({ + id: 'throw1-check1', + evaluate: function() { + throw new Error('Launch the super sheep!'); + } + }); + a.run( + { include: [axe.utils.getFlattenedTree(fixture)[0]] }, + { + runOnly: { + type: 'rule', + values: ['throw1', 'positive1'] + } + }, + function() { + done(); + }, + isNotCalled + ); + }); + + it('should run audit.normalizeOptions to ensure valid input', function() { + fixture.innerHTML = + '' + + '
bananas
' + + '' + + 'FAIL ME'; + var checked = 'options not validated'; + + a.normalizeOptions = function() { + checked = 'options validated'; + }; + + a.run({ include: [fixture] }, {}, noop, isNotCalled); + assert.equal(checked, 'options validated'); + }); + + it('should halt if an error occurs when debug is set', function(done) { + a.addRule({ + id: 'throw1', + selector: '*', + any: [ + { + id: 'throw1-check1' + } + ] + }); + a.addCheck({ + id: 'throw1-check1', + evaluate: function() { + throw new Error('Launch the super sheep!'); + } + }); + + // check error node requires _selectorCache to be setup + axe.setup(); + + a.run( + { include: [axe.utils.getFlattenedTree(fixture)[0]] }, + { + debug: true, + runOnly: { + type: 'rule', + values: ['throw1'] + } + }, + noop, + function(err) { + assert.equal(err.message, 'Launch the super sheep!'); + done(); + } + ); + }); + }); + + describe('Audit#after', function() { + it('should run Rule#after on any rule whose result is passed in', function() { + /*eslint no-unused-vars:0*/ + var audit = new Audit(); + var success = false; + var options = [{ id: 'hehe', enabled: true, monkeys: 'bananas' }]; + var results = [ + { + id: 'hehe', + monkeys: 'bananas' + } + ]; + audit.rules.push( + new Rule({ + id: 'hehe', + pageLevel: false, + enabled: false + }) + ); + + audit.rules[0].after = function(res, opts) { + assert.equal(res, results[0]); + assert.deepEqual(opts, options); + success = true; + }; + + audit.after(results, options); + }); + }); + + describe('Audit#normalizeOptions', function() { + it('returns the options object when it is valid', function() { + var opt = { + runOnly: { + type: 'rule', + values: ['positive1', 'positive2'] + }, + rules: { + negative1: { enabled: false } + } + }; + 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('allows runOnly as an array as an alternative to type: rule', function() { + var opt = { runOnly: ['positive1', 'negative1'] }; + var out = a.normalizeOptions(opt); + assert(out.runOnly.type, 'rule'); + assert.deepEqual(out.runOnly.values, ['positive1', 'negative1']); + }); + + it('throws an error if runOnly contains both rules and tags', function() { + assert.throws(function() { + a.normalizeOptions({ + runOnly: ['positive', 'negative1'] + }); + }); + }); + + it('defaults runOnly to type: tag', function() { + var opt = { runOnly: ['fakeTag'] }; + var out = a.normalizeOptions(opt); + assert(out.runOnly.type, 'tag'); + assert.deepEqual(out.runOnly.values, ['fakeTag']); + }); + + 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.normalizeOptions({ + runOnly: { + type: 'rule', + values: ['frakeRule'] + } + }); + }); + }); + + it("doesn't throw an error when option.runOnly has an unknown tag", function() { + assert.doesNotThrow(function() { + a.normalizeOptions({ + runOnly: { + type: 'tags', + values: ['fakeTag'] + } + }); + }); + }); + + it('throws an error when option.rules has an unknown rule', function() { + assert.throws(function() { + a.normalizeOptions({ + rules: { + fakeRule: { enabled: false } + } + }); + }); + }); + }); }); diff --git a/test/core/public/configure.js b/test/core/public/configure.js index 6335e4e372..0123cd77f9 100644 --- a/test/core/public/configure.js +++ b/test/core/public/configure.js @@ -1,1014 +1,1022 @@ describe('axe.configure', function() { - 'use strict'; - // var Rule = axe._thisWillBeDeletedDoNotUse.base.Rule; - // var Check = axe._thisWillBeDeletedDoNotUse.base.Check; - var fixture = document.getElementById('fixture'); - var axeVersion = axe.version; - - afterEach(function() { - fixture.innerHTML = ''; - axe.version = axeVersion; - }); - - beforeEach(function() { - axe._audit = null; - }); - - it('should throw if audit is not configured', function() { - assert.throws( - function() { - axe.configure({}); - }, - Error, - /^No audit configured/ - ); - }); - - it("should override an audit's reporter - string", function() { - axe._load({}); - assert.isNull(axe._audit.reporter); - - axe.configure({ reporter: 'v1' }); - assert.equal(axe._audit.reporter, 'v1'); - }); - - it('should not allow setting to an un-registered reporter', function() { - axe._load({ reporter: 'v1' }); - axe.configure({ reporter: 'no-exist-evar-plz' }); - assert.equal(axe._audit.reporter, 'v1'); - }); - - it('should allow for addition of rules', function() { - axe._load({}); - axe.configure({ - rules: [ - { - id: 'bob', - metadata: { - joe: 'joe' - } - } - ] - }); - - assert.lengthOf(axe._audit.rules, 1); - // TODO: this does not work yet thanks to webpack - // assert.instanceOf(axe._audit.rules[0], Rule); - assert.equal(axe._audit.rules[0].id, 'bob'); - assert.deepEqual(axe._audit.data.rules.bob.joe, 'joe'); - }); - - it('should throw error if rules property is invalid', function() { - assert.throws(function() { - axe.configure({ rules: 'hello' }), - TypeError, - /^Rules property must be an array/; - }); - }); - - it('should throw error if rule is invalid', function() { - assert.throws(function() { - axe.configure({ rules: ['hello'] }), - TypeError, - /Configured rule "hello" is invalid/; - }); - }); - - it('should throw error if rule does not have an id', function() { - assert.throws(function() { - axe.configure({ rules: [{ foo: 'bar' }] }), - TypeError, - /Configured rule "{foo:\"bar\"}" is invalid/; - }); - }); - - it('should call setBranding when passed options', function() { - axe._load({}); - axe.configure({ - rules: [ - { - id: 'bob', - selector: 'pass' - } - ], - branding: {} - }); - assert.lengthOf(axe._audit.rules, 1); - assert.equal( - axe._audit.data.rules.bob.helpUrl, - 'https://dequeuniversity.com/rules/axe/x.y/bob?application=axeAPI' - ); - axe.configure({ - branding: { - application: 'thing', - brand: 'thung' - } - }); - assert.equal( - axe._audit.data.rules.bob.helpUrl, - 'https://dequeuniversity.com/rules/thung/x.y/bob?application=thing' - ); - }); - - it('sets branding on newly configured rules', function() { - axe._load({}); - axe.configure({ - branding: { - application: 'thing', - brand: 'thung' - } - }); - axe.configure({ - rules: [ - { - id: 'bob', - selector: 'pass' - } - ] - }); - - assert.equal( - axe._audit.data.rules.bob.helpUrl, - 'https://dequeuniversity.com/rules/thung/x.y/bob?application=thing' - ); - }); - - it('should allow for overwriting of rules', function() { - axe._load({ - data: { - rules: { - bob: 'not-joe' - } - }, - rules: { - id: 'bob', - selector: 'fail' - } - }); - axe.configure({ - rules: [ - { - id: 'bob', - selector: 'pass', - metadata: { - joe: 'joe' - } - } - ] - }); - - assert.lengthOf(axe._audit.rules, 1); - // assert.instanceOf(axe._audit.rules[0], Rule); - assert.equal(axe._audit.rules[0].id, 'bob'); - assert.equal(axe._audit.rules[0].selector, 'pass'); - assert.equal(axe._audit.data.rules.bob.joe, 'joe'); - }); - - it('should allow for the addition of checks', function() { - axe._load({}); - axe.configure({ - checks: [ - { - id: 'bob', - options: { value: true }, - metadata: { - joe: 'joe' - } - } - ] - }); - - // assert.instanceOf(axe._audit.checks.bob, Check); - assert.equal(axe._audit.checks.bob.id, 'bob'); - assert.isTrue(axe._audit.checks.bob.options.value); - assert.equal(axe._audit.data.checks.bob.joe, 'joe'); - }); - - it('should throw error if checks property is invalid', function() { - assert.throws(function() { - axe.configure({ checks: 'hello' }), - TypeError, - /^Checks property must be an array/; - }); - }); - - it('should throw error if check is invalid', function() { - assert.throws(function() { - axe.configure({ checks: ['hello'] }), - TypeError, - /Configured check "hello" is invalid/; - }); - }); - - it('should throw error if check does not have an id', function() { - assert.throws(function() { - axe.configure({ checks: [{ foo: 'bar' }] }), - TypeError, - /Configured check "{foo:\"bar\"}" is invalid/; - }); - }); - - it('should allow for the overwriting of checks', function() { - axe._load({ - data: { - checks: { - bob: 'not-joe' - } - }, - checks: [ - { - id: 'bob', - options: { value: false } - } - ] - }); - axe.configure({ - checks: [ - { - id: 'bob', - options: { value: true }, - metadata: { - joe: 'joe' - } - } - ] - }); - - // assert.instanceOf(axe._audit.checks.bob, Check); - assert.equal(axe._audit.checks.bob.id, 'bob'); - assert.isTrue(axe._audit.checks.bob.options.value); - assert.equal(axe._audit.data.checks.bob.joe, 'joe'); - }); - - it('should create an execution context for check messages', function() { - axe._load({}); - axe.configure({ - checks: [ - { - id: 'bob', - metadata: { - messages: { - pass: "function () { return 'Bob' + ' John';}", - fail: 'Bob Pete' - } - } - } - ] - }); - - assert.isFunction(axe._audit.data.checks.bob.messages.pass); - assert.isString(axe._audit.data.checks.bob.messages.fail); - assert.equal(axe._audit.data.checks.bob.messages.pass(), 'Bob John'); - assert.equal(axe._audit.data.checks.bob.messages.fail, 'Bob Pete'); - }); - - it('overrides the default value of audit.tagExclude', function() { - axe._load({}); - assert.deepEqual(axe._audit.tagExclude, ['experimental']); - - axe.configure({ - tagExclude: ['ninjas'] - }); - assert.deepEqual(axe._audit.tagExclude, ['ninjas']); - }); - - it('disables all untouched rules with disableOtherRules', function() { - axe._load({ - rules: [{ id: 'captain-america' }, { id: 'thor' }, { id: 'spider-man' }] - }); - axe.configure({ - disableOtherRules: true, - rules: [{ id: 'captain-america' }, { id: 'black-panther' }] - }); - - assert.lengthOf(axe._audit.rules, 4); - assert.equal(axe._audit.rules[0].id, 'captain-america'); - assert.equal(axe._audit.rules[0].enabled, true); - assert.equal(axe._audit.rules[1].id, 'thor'); - assert.equal(axe._audit.rules[1].enabled, false); - assert.equal(axe._audit.rules[2].id, 'spider-man'); - assert.equal(axe._audit.rules[2].enabled, false); - assert.equal(axe._audit.rules[3].id, 'black-panther'); - assert.equal(axe._audit.rules[3].enabled, true); - }); - - describe('given a locale object', function() { - beforeEach(function() { - axe._load({}); - - axe.configure({ - rules: [ - { - id: 'greeting', - selector: 'div', - excludeHidden: false, - tags: ['foo', 'bar'], - metadata: { - description: 'This is a rule that rules', - help: 'ABCDEFGHIKLMNOPQRSTVXYZ' - } - } - ], - checks: [ - { - id: 'banana', - evaluate: function() {}, - metadata: { - impact: 'srsly serious', - messages: { - pass: 'yay', - fail: 'boo', - incomplete: { - foo: 'a', - bar: 'b', - baz: 'c' - } - } - } - } - ] - }); - }); - - it('should update check and rule metadata', function() { - axe.configure({ - locale: { - lang: 'lol', - rules: { - greeting: { - description: 'hello', - help: 'hi' - } - }, - checks: { - banana: { - pass: 'pizza', - fail: 'icecream', - incomplete: { - foo: 'meat', - bar: 'fruit', - baz: 'vegetables' - } - } - } - } - }); - - var audit = axe._audit; - var localeData = audit.data; - - assert.equal(localeData.rules.greeting.help, 'hi'); - assert.equal(localeData.rules.greeting.description, 'hello'); - assert.equal(localeData.checks.banana.messages.pass, 'pizza'); - assert.equal(localeData.checks.banana.messages.fail, 'icecream'); - assert.deepEqual(localeData.checks.banana.messages.incomplete, { - foo: 'meat', - bar: 'fruit', - baz: 'vegetables' - }); - }); - - it('should merge locales (favoring "new")', function() { - axe.configure({ - locale: { - lang: 'lol', - rules: { greeting: { description: 'hello' } }, - checks: { - banana: { - fail: 'icecream' - } - } - } - }); - - var audit = axe._audit; - var localeData = audit.data; - - assert.equal(localeData.rules.greeting.help, 'ABCDEFGHIKLMNOPQRSTVXYZ'); - assert.equal(localeData.rules.greeting.description, 'hello'); - assert.equal(localeData.checks.banana.messages.pass, 'yay'); - assert.equal(localeData.checks.banana.messages.fail, 'icecream'); - assert.deepEqual(localeData.checks.banana.messages.incomplete, { - foo: 'a', - bar: 'b', - baz: 'c' - }); - }); - - it('sets the lang property', function() { - axe.configure({ - locale: { - lang: 'lol', - rules: { greeting: { description: 'hello' } }, - checks: { - banana: { - fail: 'icecream' - } - } - } - }); - - assert.equal(axe._audit.lang, 'lol'); - }); - - it('should call doT.compile if a messages uses doT syntax', function() { - axe.configure({ - locale: { - lang: 'lol', - rules: { greeting: { description: 'hello' } }, - checks: { - banana: { - fail: 'icecream {{=it.data.value}}' - } - } - } - }); - - var audit = axe._audit; - var localeData = audit.data; - - assert.isTrue( - typeof localeData.checks.banana.messages.fail === 'function' - ); - }); - - it('should leave the messages as a string if it does not use doT syntax', function() { - axe.configure({ - locale: { - lang: 'lol', - rules: { greeting: { description: 'hello' } }, - checks: { - banana: { - fail: 'icecream ${data.value}' - } - } - } - }); - - var audit = axe._audit; - var localeData = audit.data; - - assert.isTrue(typeof localeData.checks.banana.messages.fail === 'string'); - }); - - it('should update failure messages', function() { - axe._load({ - data: { - failureSummaries: { - any: { - failureMessage: function() { - return 'failed any'; - } - }, - none: { - failureMessage: function() { - return 'failed none'; - } - } - }, - incompleteFallbackMessage: function() { - return 'failed incomplete'; - } - } - }); - - axe.configure({ - locale: { - lang: 'lol', - failureSummaries: { - any: { - failureMessage: 'foo' - }, - none: { - failureMessage: 'bar' - } - }, - incompleteFallbackMessage: 'baz' - } - }); - - var audit = axe._audit; - var localeData = audit.data; - - assert.equal(localeData.failureSummaries.any.failureMessage, 'foo'); - assert.equal(localeData.failureSummaries.none.failureMessage, 'bar'); - assert.equal(localeData.incompleteFallbackMessage, 'baz'); - }); - - it('should merge failure messages', function() { - axe._load({ - data: { - failureSummaries: { - any: { - failureMessage: function() { - return 'failed any'; - } - }, - none: { - failureMessage: function() { - return 'failed none'; - } - } - }, - incompleteFallbackMessage: function() { - return 'failed incomplete'; - } - } - }); - - axe.configure({ - locale: { - lang: 'lol', - failureSummaries: { - any: { - failureMessage: 'foo' - } - } - } - }); - - var audit = axe._audit; - var localeData = audit.data; - - assert.equal(localeData.failureSummaries.any.failureMessage, 'foo'); - assert.equal( - localeData.failureSummaries.none.failureMessage(), - 'failed none' - ); - assert.equal(localeData.incompleteFallbackMessage(), 'failed incomplete'); - }); - - describe('only given checks', function() { - it('should not error', function() { - assert.doesNotThrow(function() { - axe.configure({ - locale: { - lang: 'lol', - checks: { - banana: { - fail: 'icecream', - incomplete: { - baz: 'vegetables' - } - } - } - } - }); - }); - }); - }); - - describe('only given rules', function() { - it('should not error', function() { - assert.doesNotThrow(function() { - axe.configure({ - locale: { - rules: { greeting: { help: 'foo', description: 'bar' } } - } - }); - }); - }); - }); - - describe('check incomplete messages', function() { - beforeEach(function() { - axe.configure({ - checks: [ - { - id: 'panda', - evaluate: function() {}, - metadata: { - impact: 'yep', - messages: { - pass: 'p', - fail: 'f', - incomplete: 'i' - } - } - } - ] - }); - }); - - it('should support strings', function() { - axe.configure({ - locale: { - checks: { - panda: { - incomplete: 'radio' - } - } - } - }); - - assert.equal(axe._audit.data.checks.panda.messages.incomplete, 'radio'); - }); - - it('should shallow-merge objects', function() { - axe.configure({ - locale: { - lang: 'lol', - checks: { - banana: { - incomplete: { - baz: 'vegetables' - } - } - } - } - }); - - assert.deepEqual(axe._audit.data.checks.banana.messages.incomplete, { - foo: 'a', - bar: 'b', - baz: 'vegetables' - }); - }); - }); - - // This test ensures we do not drop additional properties added to - // checks. See https://github.com/dequelabs/axe-core/pull/1036/files#r207738673 - // for reasoning. - it('should keep existing properties on check data', function() { - axe.configure({ - checks: [ - { - id: 'banana', - metadata: { - impact: 'potato', - foo: 'bar', - messages: { - pass: 'pass', - fail: 'fail', - incomplete: 'incomplete' - } - } - } - ] - }); - - axe.configure({ - locale: { - lang: 'lol', - checks: { - banana: { - pass: 'yay banana' - } - } - } - }); - - var banana = axe._audit.data.checks.banana; - assert.equal(banana.impact, 'potato'); - assert.equal(banana.foo, 'bar'); - assert.equal(banana.messages.pass, 'yay banana'); - }); - - it('should error when provided an unknown rule id', function() { - assert.throws(function() { - axe.configure({ - locale: { - rules: { nope: { help: 'helpme' } } - } - }); - }, /unknown rule: "nope"/); - }); - - it('should error when provided an unknown check id', function() { - assert.throws(function() { - axe.configure({ - locale: { - checks: { nope: { pass: 'helpme' } } - } - }); - }, /unknown check: "nope"/); - }); - - it('should error when provided an unknown failure summary', function() { - assert.throws(function() { - axe.configure({ - locale: { - failureSummaries: { - nope: { failureMessage: 'helpme' } - } - } - }); - }); - }); - - it('should set default locale', function() { - assert.isNull(axe._audit._defaultLocale); - axe.configure({ - locale: { - lang: 'lol', - checks: { - banana: { - pass: 'yay banana' - } - } - } - }); - assert.ok(axe._audit._defaultLocale); - }); - - describe('also given metadata', function() { - it('should favor the locale', function() { - axe.configure({ - locale: { - lang: 'lol', - rules: { - greeting: { - help: 'hi' - } - } - }, - rules: [ - { - id: 'greeting', - metadata: { - help: 'potato' - } - } - ] - }); - - var audit = axe._audit; - var localeData = audit.data; - - assert.equal(localeData.rules.greeting.help, 'hi'); - }); - }); - - describe('after locale has been set', function() { - describe('the provided messages', function() { - it('should allow for doT templating', function() { - axe.configure({ - locale: { - lang: 'foo', - rules: { - greeting: { - help: 'foo: {{=it.data}}.' - } - } - } - }); - - var greeting = axe._audit.data.rules.greeting; - var value = greeting.help({ - data: 'bar' - }); - assert.equal(value, 'foo: bar.'); - }); - }); - }); - }); - - describe('given an axeVersion property', function() { - beforeEach(function() { - axe._load({}); - axe.version = '1.2.3'; - }); - - it('should not throw if version matches axe.version', function() { - assert.doesNotThrow(function fn() { - axe.configure({ - axeVersion: '1.2.3' - }); - - axe.version = '1.2.3-canary.2664bae'; - axe.configure({ - axeVersion: '1.2.3-canary.2664bae' - }); - }); - }); - - it('should not throw if patch version is less than axe.version', function() { - assert.doesNotThrow(function fn() { - axe.configure({ - axeVersion: '1.2.0' - }); - }); - }); - - it('should not throw if minor version is less than axe.version', function() { - assert.doesNotThrow(function fn() { - axe.configure({ - axeVersion: '1.1.9' - }); - }); - }); - - it('should not throw if versions match and axe has a canary version', function() { - axe.version = '1.2.3-canary.2664bae'; - assert.doesNotThrow(function fn() { - axe.configure({ - axeVersion: '1.2.3' - }); - }); - }); - - it('should throw if invalid version', function() { - assert.throws(function fn() { - axe.configure({ - axeVersion: '2' - }); - }, 'Invalid configured version 2'); - - assert.throws(function fn() { - axe.configure({ - axeVersion: '2..' - }); - }, 'Invalid configured version 2..'); - }); - - it('should throw if major version is different than axe.version', function() { - assert.throws(function fn() { - axe.configure( - { - axeVersion: '2.0.0' - }, - /^Configured version/ - ); - }); - assert.throws(function fn() { - axe.configure( - { - axeVersion: '0.1.2' - }, - /^Configured version/ - ); - }); - }); - - it('should throw if minor version is greater than axe.version', function() { - assert.throws(function fn() { - axe.configure( - { - axeVersion: '1.3.0' - }, - /^Configured version/ - ); - }); - }); - - it('should throw if patch version is greater than axe.version', function() { - assert.throws(function fn() { - axe.configure( - { - axeVersion: '1.2.9' - }, - /^Configured version/ - ); - }); - }); - - it('should throw if versions match and axeVersion has a canary version', function() { - assert.throws(function fn() { - axe.configure( - { - axeVersion: '1.2.3-canary.2664bae' - }, - /^Configured version/ - ); - }); - }); - - it('should throw if versions match and both have a canary version', function() { - axe.version = '1.2.3-canary.2664bae'; - assert.throws(function fn() { - axe.configure( - { - axeVersion: '1.2.3-canary.a5d727c' - }, - /^Configured version/ - ); - }); - }); - - it('should accept ver property as fallback', function() { - assert.throws(function fn() { - axe.configure( - { - ver: '1.3.0' - }, - /^Configured version/ - ); - }); - }); - - it('should accept axeVersion over ver property', function() { - assert.throws(function fn() { - axe.configure( - { - ver: '0.1.2', - axeVersion: '1.3.0' - }, - /^Configured version 1\.3\.0/ - ); - }); - }); - }); - - describe('given a standards object', function() { - beforeEach(function() { - axe._load({}); - }); - - describe('ariaAttrs', function() { - it('should allow creating new attr', function() { - axe.configure({ - standards: { - ariaAttrs: { - newAttr: { - type: 'string' - } - } - } - }); - - var ariaAttr = axe._audit.standards.ariaAttrs.newAttr; - assert.equal(ariaAttr.type, 'string'); - }); - - it('should override existing attr', function() { - axe.configure({ - standards: { - ariaAttrs: { - newAttr: { - type: 'string' - } - } - } - }); - - axe.configure({ - standards: { - ariaAttrs: { - newAttr: { - type: 'mntoken', - values: ['foo', 'bar'] - } - } - } - }); - - var ariaAttr = axe._audit.standards.ariaAttrs.newAttr; - assert.equal(ariaAttr.type, 'mntoken'); - assert.deepEqual(ariaAttr.values, ['foo', 'bar']); - }); - - it('should merge existing attr', function() { - axe.configure({ - standards: { - ariaAttrs: { - newAttr: { - type: 'mntoken', - values: ['foo', 'bar'] - } - } - } - }); - - axe.configure({ - standards: { - ariaAttrs: { - newAttr: { - type: 'mntokens' - } - } - } - }); - - var ariaAttr = axe._audit.standards.ariaAttrs.newAttr; - assert.equal(ariaAttr.type, 'mntokens'); - assert.deepEqual(ariaAttr.values, ['foo', 'bar']); - }); - - it('should override and not merge array', function() { - axe.configure({ - standards: { - ariaAttrs: { - newAttr: { - type: 'mntoken', - values: ['foo', 'bar'] - } - } - } - }); - - axe.configure({ - standards: { - ariaAttrs: { - newAttr: { - values: ['baz'] - } - } - } - }); - - var ariaAttr = axe._audit.standards.ariaAttrs.newAttr; - assert.deepEqual(ariaAttr.values, ['baz']); - }); - }); - }); + 'use strict'; + // var Rule = axe._thisWillBeDeletedDoNotUse.base.Rule; + // var Check = axe._thisWillBeDeletedDoNotUse.base.Check; + var fixture = document.getElementById('fixture'); + var axeVersion = axe.version; + + afterEach(function() { + fixture.innerHTML = ''; + axe.version = axeVersion; + }); + + beforeEach(function() { + axe._audit = null; + }); + + it('should throw if audit is not configured', function() { + assert.throws( + function() { + axe.configure({}); + }, + Error, + /^No audit configured/ + ); + }); + + it("should override an audit's reporter - string", function() { + axe._load({}); + assert.isNull(axe._audit.reporter); + + axe.configure({ reporter: 'v1' }); + assert.equal(axe._audit.reporter, 'v1'); + }); + + it('should not allow setting to an un-registered reporter', function() { + axe._load({ reporter: 'v1' }); + axe.configure({ reporter: 'no-exist-evar-plz' }); + assert.equal(axe._audit.reporter, 'v1'); + }); + + it('should allow for addition of rules', function() { + axe._load({}); + axe.configure({ + rules: [ + { + id: 'bob', + metadata: { + joe: 'joe' + } + } + ] + }); + + assert.lengthOf(axe._audit.rules, 1); + // TODO: this does not work yet thanks to webpack + // assert.instanceOf(axe._audit.rules[0], Rule); + assert.equal(axe._audit.rules[0].id, 'bob'); + assert.deepEqual(axe._audit.data.rules.bob.joe, 'joe'); + }); + + it('should throw error if rules property is invalid', function() { + assert.throws(function() { + axe.configure({ rules: 'hello' }), + TypeError, + /^Rules property must be an array/; + }); + }); + + it('should throw error if rule is invalid', function() { + assert.throws(function() { + axe.configure({ rules: ['hello'] }), + TypeError, + /Configured rule "hello" is invalid/; + }); + }); + + it('should throw error if rule does not have an id', function() { + assert.throws(function() { + axe.configure({ rules: [{ foo: 'bar' }] }), + TypeError, + /Configured rule "{foo:\"bar\"}" is invalid/; + }); + }); + + it('should call setBranding when passed options', function() { + axe._load({}); + axe.configure({ + rules: [ + { + id: 'bob', + selector: 'pass' + } + ], + branding: {} + }); + assert.lengthOf(axe._audit.rules, 1); + assert.equal( + axe._audit.data.rules.bob.helpUrl, + 'https://dequeuniversity.com/rules/axe/x.y/bob?application=axeAPI' + ); + axe.configure({ + branding: { + application: 'thing', + brand: 'thung' + } + }); + assert.equal( + axe._audit.data.rules.bob.helpUrl, + 'https://dequeuniversity.com/rules/thung/x.y/bob?application=thing' + ); + }); + + it('sets branding on newly configured rules', function() { + axe._load({}); + axe.configure({ + branding: { + application: 'thing', + brand: 'thung' + } + }); + axe.configure({ + rules: [ + { + id: 'bob', + selector: 'pass' + } + ] + }); + + assert.equal( + axe._audit.data.rules.bob.helpUrl, + 'https://dequeuniversity.com/rules/thung/x.y/bob?application=thing' + ); + }); + + it('should allow for overwriting of rules', function() { + axe._load({ + data: { + rules: { + bob: 'not-joe' + } + }, + rules: { + id: 'bob', + selector: 'fail' + } + }); + axe.configure({ + rules: [ + { + id: 'bob', + selector: 'pass', + metadata: { + joe: 'joe' + } + } + ] + }); + + assert.lengthOf(axe._audit.rules, 1); + // assert.instanceOf(axe._audit.rules[0], Rule); + assert.equal(axe._audit.rules[0].id, 'bob'); + assert.equal(axe._audit.rules[0].selector, 'pass'); + assert.equal(axe._audit.data.rules.bob.joe, 'joe'); + }); + + it('should allow for the addition of checks', function() { + axe._load({}); + axe.configure({ + checks: [ + { + id: 'bob', + options: { value: true }, + metadata: { + joe: 'joe' + } + } + ] + }); + + // assert.instanceOf(axe._audit.checks.bob, Check); + assert.equal(axe._audit.checks.bob.id, 'bob'); + assert.isTrue(axe._audit.checks.bob.options.value); + assert.equal(axe._audit.data.checks.bob.joe, 'joe'); + }); + + it('should throw error if checks property is invalid', function() { + assert.throws(function() { + axe.configure({ checks: 'hello' }), + TypeError, + /^Checks property must be an array/; + }); + }); + + it('should throw error if check is invalid', function() { + assert.throws(function() { + axe.configure({ checks: ['hello'] }), + TypeError, + /Configured check "hello" is invalid/; + }); + }); + + it('should throw error if check does not have an id', function() { + assert.throws(function() { + axe.configure({ checks: [{ foo: 'bar' }] }), + TypeError, + /Configured check "{foo:\"bar\"}" is invalid/; + }); + }); + + it('should allow for the overwriting of checks', function() { + axe._load({ + data: { + checks: { + bob: 'not-joe' + } + }, + checks: [ + { + id: 'bob', + options: { value: false } + } + ] + }); + axe.configure({ + checks: [ + { + id: 'bob', + options: { value: true }, + metadata: { + joe: 'joe' + } + } + ] + }); + + // assert.instanceOf(axe._audit.checks.bob, Check); + assert.equal(axe._audit.checks.bob.id, 'bob'); + assert.isTrue(axe._audit.checks.bob.options.value); + assert.equal(axe._audit.data.checks.bob.joe, 'joe'); + }); + + it('should create an execution context for check messages', function() { + axe._load({}); + axe.configure({ + checks: [ + { + id: 'bob', + metadata: { + messages: { + pass: "function () { return 'Bob' + ' John';}", + fail: 'Bob Pete' + } + } + } + ] + }); + + assert.isFunction(axe._audit.data.checks.bob.messages.pass); + assert.isString(axe._audit.data.checks.bob.messages.fail); + assert.equal(axe._audit.data.checks.bob.messages.pass(), 'Bob John'); + assert.equal(axe._audit.data.checks.bob.messages.fail, 'Bob Pete'); + }); + + it('overrides the default value of audit.tagExclude', function() { + axe._load({}); + assert.deepEqual(axe._audit.tagExclude, ['experimental']); + + axe.configure({ + tagExclude: ['ninjas'] + }); + assert.deepEqual(axe._audit.tagExclude, ['ninjas']); + }); + + it('disables all untouched rules with disableOtherRules', function() { + axe._load({ + rules: [{ id: 'captain-america' }, { id: 'thor' }, { id: 'spider-man' }] + }); + axe.configure({ + disableOtherRules: true, + rules: [{ id: 'captain-america' }, { id: 'black-panther' }] + }); + + assert.lengthOf(axe._audit.rules, 4); + assert.equal(axe._audit.rules[0].id, 'captain-america'); + assert.equal(axe._audit.rules[0].enabled, true); + assert.equal(axe._audit.rules[1].id, 'thor'); + assert.equal(axe._audit.rules[1].enabled, false); + assert.equal(axe._audit.rules[2].id, 'spider-man'); + assert.equal(axe._audit.rules[2].enabled, false); + assert.equal(axe._audit.rules[3].id, 'black-panther'); + assert.equal(axe._audit.rules[3].enabled, true); + }); + + it("should allow overriding an audit's noHtml", function() { + axe._load({}); + assert.isFalse(axe._audit.noHtml); + + axe.configure({ noHtml: true }); + assert.isTrue(axe._audit.noHtml); + }); + + describe('given a locale object', function() { + beforeEach(function() { + axe._load({}); + + axe.configure({ + rules: [ + { + id: 'greeting', + selector: 'div', + excludeHidden: false, + tags: ['foo', 'bar'], + metadata: { + description: 'This is a rule that rules', + help: 'ABCDEFGHIKLMNOPQRSTVXYZ' + } + } + ], + checks: [ + { + id: 'banana', + evaluate: function() {}, + metadata: { + impact: 'srsly serious', + messages: { + pass: 'yay', + fail: 'boo', + incomplete: { + foo: 'a', + bar: 'b', + baz: 'c' + } + } + } + } + ] + }); + }); + + it('should update check and rule metadata', function() { + axe.configure({ + locale: { + lang: 'lol', + rules: { + greeting: { + description: 'hello', + help: 'hi' + } + }, + checks: { + banana: { + pass: 'pizza', + fail: 'icecream', + incomplete: { + foo: 'meat', + bar: 'fruit', + baz: 'vegetables' + } + } + } + } + }); + + var audit = axe._audit; + var localeData = audit.data; + + assert.equal(localeData.rules.greeting.help, 'hi'); + assert.equal(localeData.rules.greeting.description, 'hello'); + assert.equal(localeData.checks.banana.messages.pass, 'pizza'); + assert.equal(localeData.checks.banana.messages.fail, 'icecream'); + assert.deepEqual(localeData.checks.banana.messages.incomplete, { + foo: 'meat', + bar: 'fruit', + baz: 'vegetables' + }); + }); + + it('should merge locales (favoring "new")', function() { + axe.configure({ + locale: { + lang: 'lol', + rules: { greeting: { description: 'hello' } }, + checks: { + banana: { + fail: 'icecream' + } + } + } + }); + + var audit = axe._audit; + var localeData = audit.data; + + assert.equal(localeData.rules.greeting.help, 'ABCDEFGHIKLMNOPQRSTVXYZ'); + assert.equal(localeData.rules.greeting.description, 'hello'); + assert.equal(localeData.checks.banana.messages.pass, 'yay'); + assert.equal(localeData.checks.banana.messages.fail, 'icecream'); + assert.deepEqual(localeData.checks.banana.messages.incomplete, { + foo: 'a', + bar: 'b', + baz: 'c' + }); + }); + + it('sets the lang property', function() { + axe.configure({ + locale: { + lang: 'lol', + rules: { greeting: { description: 'hello' } }, + checks: { + banana: { + fail: 'icecream' + } + } + } + }); + + assert.equal(axe._audit.lang, 'lol'); + }); + + it('should call doT.compile if a messages uses doT syntax', function() { + axe.configure({ + locale: { + lang: 'lol', + rules: { greeting: { description: 'hello' } }, + checks: { + banana: { + fail: 'icecream {{=it.data.value}}' + } + } + } + }); + + var audit = axe._audit; + var localeData = audit.data; + + assert.isTrue( + typeof localeData.checks.banana.messages.fail === 'function' + ); + }); + + it('should leave the messages as a string if it does not use doT syntax', function() { + axe.configure({ + locale: { + lang: 'lol', + rules: { greeting: { description: 'hello' } }, + checks: { + banana: { + fail: 'icecream ${data.value}' + } + } + } + }); + + var audit = axe._audit; + var localeData = audit.data; + + assert.isTrue(typeof localeData.checks.banana.messages.fail === 'string'); + }); + + it('should update failure messages', function() { + axe._load({ + data: { + failureSummaries: { + any: { + failureMessage: function() { + return 'failed any'; + } + }, + none: { + failureMessage: function() { + return 'failed none'; + } + } + }, + incompleteFallbackMessage: function() { + return 'failed incomplete'; + } + } + }); + + axe.configure({ + locale: { + lang: 'lol', + failureSummaries: { + any: { + failureMessage: 'foo' + }, + none: { + failureMessage: 'bar' + } + }, + incompleteFallbackMessage: 'baz' + } + }); + + var audit = axe._audit; + var localeData = audit.data; + + assert.equal(localeData.failureSummaries.any.failureMessage, 'foo'); + assert.equal(localeData.failureSummaries.none.failureMessage, 'bar'); + assert.equal(localeData.incompleteFallbackMessage, 'baz'); + }); + + it('should merge failure messages', function() { + axe._load({ + data: { + failureSummaries: { + any: { + failureMessage: function() { + return 'failed any'; + } + }, + none: { + failureMessage: function() { + return 'failed none'; + } + } + }, + incompleteFallbackMessage: function() { + return 'failed incomplete'; + } + } + }); + + axe.configure({ + locale: { + lang: 'lol', + failureSummaries: { + any: { + failureMessage: 'foo' + } + } + } + }); + + var audit = axe._audit; + var localeData = audit.data; + + assert.equal(localeData.failureSummaries.any.failureMessage, 'foo'); + assert.equal( + localeData.failureSummaries.none.failureMessage(), + 'failed none' + ); + assert.equal(localeData.incompleteFallbackMessage(), 'failed incomplete'); + }); + + describe('only given checks', function() { + it('should not error', function() { + assert.doesNotThrow(function() { + axe.configure({ + locale: { + lang: 'lol', + checks: { + banana: { + fail: 'icecream', + incomplete: { + baz: 'vegetables' + } + } + } + } + }); + }); + }); + }); + + describe('only given rules', function() { + it('should not error', function() { + assert.doesNotThrow(function() { + axe.configure({ + locale: { + rules: { greeting: { help: 'foo', description: 'bar' } } + } + }); + }); + }); + }); + + describe('check incomplete messages', function() { + beforeEach(function() { + axe.configure({ + checks: [ + { + id: 'panda', + evaluate: function() {}, + metadata: { + impact: 'yep', + messages: { + pass: 'p', + fail: 'f', + incomplete: 'i' + } + } + } + ] + }); + }); + + it('should support strings', function() { + axe.configure({ + locale: { + checks: { + panda: { + incomplete: 'radio' + } + } + } + }); + + assert.equal(axe._audit.data.checks.panda.messages.incomplete, 'radio'); + }); + + it('should shallow-merge objects', function() { + axe.configure({ + locale: { + lang: 'lol', + checks: { + banana: { + incomplete: { + baz: 'vegetables' + } + } + } + } + }); + + assert.deepEqual(axe._audit.data.checks.banana.messages.incomplete, { + foo: 'a', + bar: 'b', + baz: 'vegetables' + }); + }); + }); + + // This test ensures we do not drop additional properties added to + // checks. See https://github.com/dequelabs/axe-core/pull/1036/files#r207738673 + // for reasoning. + it('should keep existing properties on check data', function() { + axe.configure({ + checks: [ + { + id: 'banana', + metadata: { + impact: 'potato', + foo: 'bar', + messages: { + pass: 'pass', + fail: 'fail', + incomplete: 'incomplete' + } + } + } + ] + }); + + axe.configure({ + locale: { + lang: 'lol', + checks: { + banana: { + pass: 'yay banana' + } + } + } + }); + + var banana = axe._audit.data.checks.banana; + assert.equal(banana.impact, 'potato'); + assert.equal(banana.foo, 'bar'); + assert.equal(banana.messages.pass, 'yay banana'); + }); + + it('should error when provided an unknown rule id', function() { + assert.throws(function() { + axe.configure({ + locale: { + rules: { nope: { help: 'helpme' } } + } + }); + }, /unknown rule: "nope"/); + }); + + it('should error when provided an unknown check id', function() { + assert.throws(function() { + axe.configure({ + locale: { + checks: { nope: { pass: 'helpme' } } + } + }); + }, /unknown check: "nope"/); + }); + + it('should error when provided an unknown failure summary', function() { + assert.throws(function() { + axe.configure({ + locale: { + failureSummaries: { + nope: { failureMessage: 'helpme' } + } + } + }); + }); + }); + + it('should set default locale', function() { + assert.isNull(axe._audit._defaultLocale); + axe.configure({ + locale: { + lang: 'lol', + checks: { + banana: { + pass: 'yay banana' + } + } + } + }); + assert.ok(axe._audit._defaultLocale); + }); + + describe('also given metadata', function() { + it('should favor the locale', function() { + axe.configure({ + locale: { + lang: 'lol', + rules: { + greeting: { + help: 'hi' + } + } + }, + rules: [ + { + id: 'greeting', + metadata: { + help: 'potato' + } + } + ] + }); + + var audit = axe._audit; + var localeData = audit.data; + + assert.equal(localeData.rules.greeting.help, 'hi'); + }); + }); + + describe('after locale has been set', function() { + describe('the provided messages', function() { + it('should allow for doT templating', function() { + axe.configure({ + locale: { + lang: 'foo', + rules: { + greeting: { + help: 'foo: {{=it.data}}.' + } + } + } + }); + + var greeting = axe._audit.data.rules.greeting; + var value = greeting.help({ + data: 'bar' + }); + assert.equal(value, 'foo: bar.'); + }); + }); + }); + }); + + describe('given an axeVersion property', function() { + beforeEach(function() { + axe._load({}); + axe.version = '1.2.3'; + }); + + it('should not throw if version matches axe.version', function() { + assert.doesNotThrow(function fn() { + axe.configure({ + axeVersion: '1.2.3' + }); + + axe.version = '1.2.3-canary.2664bae'; + axe.configure({ + axeVersion: '1.2.3-canary.2664bae' + }); + }); + }); + + it('should not throw if patch version is less than axe.version', function() { + assert.doesNotThrow(function fn() { + axe.configure({ + axeVersion: '1.2.0' + }); + }); + }); + + it('should not throw if minor version is less than axe.version', function() { + assert.doesNotThrow(function fn() { + axe.configure({ + axeVersion: '1.1.9' + }); + }); + }); + + it('should not throw if versions match and axe has a canary version', function() { + axe.version = '1.2.3-canary.2664bae'; + assert.doesNotThrow(function fn() { + axe.configure({ + axeVersion: '1.2.3' + }); + }); + }); + + it('should throw if invalid version', function() { + assert.throws(function fn() { + axe.configure({ + axeVersion: '2' + }); + }, 'Invalid configured version 2'); + + assert.throws(function fn() { + axe.configure({ + axeVersion: '2..' + }); + }, 'Invalid configured version 2..'); + }); + + it('should throw if major version is different than axe.version', function() { + assert.throws(function fn() { + axe.configure( + { + axeVersion: '2.0.0' + }, + /^Configured version/ + ); + }); + assert.throws(function fn() { + axe.configure( + { + axeVersion: '0.1.2' + }, + /^Configured version/ + ); + }); + }); + + it('should throw if minor version is greater than axe.version', function() { + assert.throws(function fn() { + axe.configure( + { + axeVersion: '1.3.0' + }, + /^Configured version/ + ); + }); + }); + + it('should throw if patch version is greater than axe.version', function() { + assert.throws(function fn() { + axe.configure( + { + axeVersion: '1.2.9' + }, + /^Configured version/ + ); + }); + }); + + it('should throw if versions match and axeVersion has a canary version', function() { + assert.throws(function fn() { + axe.configure( + { + axeVersion: '1.2.3-canary.2664bae' + }, + /^Configured version/ + ); + }); + }); + + it('should throw if versions match and both have a canary version', function() { + axe.version = '1.2.3-canary.2664bae'; + assert.throws(function fn() { + axe.configure( + { + axeVersion: '1.2.3-canary.a5d727c' + }, + /^Configured version/ + ); + }); + }); + + it('should accept ver property as fallback', function() { + assert.throws(function fn() { + axe.configure( + { + ver: '1.3.0' + }, + /^Configured version/ + ); + }); + }); + + it('should accept axeVersion over ver property', function() { + assert.throws(function fn() { + axe.configure( + { + ver: '0.1.2', + axeVersion: '1.3.0' + }, + /^Configured version 1\.3\.0/ + ); + }); + }); + }); + + describe('given a standards object', function() { + beforeEach(function() { + axe._load({}); + }); + + describe('ariaAttrs', function() { + it('should allow creating new attr', function() { + axe.configure({ + standards: { + ariaAttrs: { + newAttr: { + type: 'string' + } + } + } + }); + + var ariaAttr = axe._audit.standards.ariaAttrs.newAttr; + assert.equal(ariaAttr.type, 'string'); + }); + + it('should override existing attr', function() { + axe.configure({ + standards: { + ariaAttrs: { + newAttr: { + type: 'string' + } + } + } + }); + + axe.configure({ + standards: { + ariaAttrs: { + newAttr: { + type: 'mntoken', + values: ['foo', 'bar'] + } + } + } + }); + + var ariaAttr = axe._audit.standards.ariaAttrs.newAttr; + assert.equal(ariaAttr.type, 'mntoken'); + assert.deepEqual(ariaAttr.values, ['foo', 'bar']); + }); + + it('should merge existing attr', function() { + axe.configure({ + standards: { + ariaAttrs: { + newAttr: { + type: 'mntoken', + values: ['foo', 'bar'] + } + } + } + }); + + axe.configure({ + standards: { + ariaAttrs: { + newAttr: { + type: 'mntokens' + } + } + } + }); + + var ariaAttr = axe._audit.standards.ariaAttrs.newAttr; + assert.equal(ariaAttr.type, 'mntokens'); + assert.deepEqual(ariaAttr.values, ['foo', 'bar']); + }); + + it('should override and not merge array', function() { + axe.configure({ + standards: { + ariaAttrs: { + newAttr: { + type: 'mntoken', + values: ['foo', 'bar'] + } + } + } + }); + + axe.configure({ + standards: { + ariaAttrs: { + newAttr: { + values: ['baz'] + } + } + } + }); + + var ariaAttr = axe._audit.standards.ariaAttrs.newAttr; + assert.deepEqual(ariaAttr.values, ['baz']); + }); + }); + }); }); diff --git a/test/core/utils/dq-element.js b/test/core/utils/dq-element.js index a3f39ad9a5..fee058d451 100644 --- a/test/core/utils/dq-element.js +++ b/test/core/utils/dq-element.js @@ -1,212 +1,230 @@ describe('DqElement', function() { - 'use strict'; - - var DqElement = axe.utils.DqElement; - var fixture = document.getElementById('fixture'); - var fixtureSetup = axe.testUtils.fixtureSetup; - - afterEach(function() { - fixture.innerHTML = ''; - axe._tree = undefined; - axe._selectorData = undefined; - }); - - it('should be a function', function() { - assert.isFunction(DqElement); - }); - - it('should be exposed to utils', function() { - assert.equal(axe.utils.DqElement, DqElement); - }); - - it('should take a node as a parameter and return an object', function() { - var node = document.createElement('div'); - var result = new DqElement(node); - - assert.isObject(result); - }); - describe('element', function() { - it('should store reference to the element', function() { - var div = document.createElement('div'); - var dqEl = new DqElement(div); - assert.equal(dqEl.element, div); - }); - - it('should not be present in stringified version', function() { - var div = document.createElement('div'); - fixtureSetup(); - - var dqEl = new DqElement(div); - - assert.isUndefined(JSON.parse(JSON.stringify(dqEl)).element); - }); - }); - - describe('source', function() { - it('should include the outerHTML of the element', function() { - fixture.innerHTML = '
Hello!
'; - - var result = new DqElement(fixture.firstChild); - assert.equal(result.source, fixture.firstChild.outerHTML); - }); - - it('should work with SVG elements', function() { - fixture.innerHTML = ''; - - var result = new DqElement(fixture.firstChild); - assert.isString(result.source); - }); - it('should work with MathML', function() { - fixture.innerHTML = - 'x2'; - - var result = new DqElement(fixture.firstChild); - assert.isString(result.source); - }); - - it('should truncate large elements', function() { - var div = '
'; - for (var i = 0; i < 300; i++) { - div += i; - } - div += '
'; - fixture.innerHTML = div; - - var result = new DqElement(fixture.firstChild); - assert.equal(result.source.length, '
'.length); - }); - - it('should use spec object over passed element', function() { - fixture.innerHTML = '
Hello!
'; - var result = new DqElement( - fixture.firstChild, - {}, - { - source: 'woot' - } - ); - assert.equal(result.source, 'woot'); - }); - }); - - describe('selector', function() { - it('should prefer selector from spec object', function() { - fixture.innerHTML = '
Hello!
'; - var result = new DqElement( - fixture.firstChild, - {}, - { - selector: 'woot' - } - ); - assert.equal(result.selector, 'woot'); - }); - }); - - describe('ancestry', function() { - it('should prefer selector from spec object', function() { - fixture.innerHTML = '
Hello!
'; - var result = new DqElement( - fixture.firstChild, - {}, - { - ancestry: 'woot' - } - ); - assert.equal(result.ancestry, 'woot'); - }); - }); - - describe('xpath', function() { - it('should prefer selector from spec object', function() { - fixture.innerHTML = '
Hello!
'; - var result = new DqElement( - fixture.firstChild, - {}, - { - xpath: 'woot' - } - ); - assert.equal(result.xpath, 'woot'); - }); - }); - - describe('absolutePaths', function() { - it('creates a path all the way to root', function() { - fixtureSetup('
Hello!
'); - - var result = new DqElement(fixture.firstChild, { - absolutePaths: true - }); - assert.include(result.selector[0], 'html > '); - assert.include(result.selector[0], '#fixture > '); - assert.include(result.selector[0], '#foo'); - }); - }); - - describe('toJSON', function() { - it('should only stringify selector and source', function() { - var expected = { - selector: 'foo > bar > joe', - source: '', - xpath: '/foo/bar/joe', - ancestry: 'foo > bar > joe' - }; - var result = new DqElement('joe', {}, expected); - - assert.deepEqual(JSON.stringify(result), JSON.stringify(expected)); - }); - }); - - describe('fromFrame', function() { - var dqMain, dqIframe; - beforeEach(function() { - var main = document.createElement('main'); - main.id = 'main'; - dqMain = new DqElement( - main, - {}, - { - selector: ['#main'], - ancestry: ['html > body > main'], - xpath: ['/main'] - } - ); - - var iframe = document.createElement('iframe'); - iframe.id = 'iframe'; - dqIframe = new DqElement( - iframe, - {}, - { - selector: ['#iframe'], - ancestry: ['html > body > iframe'], - xpath: ['/iframe'] - } - ); - }); - - it('returns a new DqElement', function() { - assert.instanceOf(DqElement.fromFrame(dqMain, {}, dqIframe), DqElement); - }); - - it('sets options for DqElement', function() { - var options = { absolutePaths: true }; - var dqElm = DqElement.fromFrame(dqMain, options, dqIframe); - assert.isTrue(dqElm._options.toRoot); - }); - - it('merges node and frame selectors', function() { - var dqElm = DqElement.fromFrame(dqMain, {}, dqIframe); - assert.deepEqual(dqElm.selector, [ - dqIframe.selector[0], - dqMain.selector[0] - ]); - assert.deepEqual(dqElm.ancestry, [ - dqIframe.ancestry[0], - dqMain.ancestry[0] - ]); - assert.deepEqual(dqElm.xpath, [dqIframe.xpath[0], dqMain.xpath[0]]); - }); - }); + 'use strict'; + + var DqElement = axe.utils.DqElement; + var fixture = document.getElementById('fixture'); + var fixtureSetup = axe.testUtils.fixtureSetup; + + afterEach(function() { + axe.reset(); + }); + + it('should be a function', function() { + assert.isFunction(DqElement); + }); + + it('should be exposed to utils', function() { + assert.equal(axe.utils.DqElement, DqElement); + }); + + it('should take a node as a parameter and return an object', function() { + var node = document.createElement('div'); + var result = new DqElement(node); + + assert.isObject(result); + }); + describe('element', function() { + it('should store reference to the element', function() { + var div = document.createElement('div'); + var dqEl = new DqElement(div); + assert.equal(dqEl.element, div); + }); + + it('should not be present in stringified version', function() { + var div = document.createElement('div'); + fixtureSetup(); + + var dqEl = new DqElement(div); + + assert.isUndefined(JSON.parse(JSON.stringify(dqEl)).element); + }); + }); + + describe('source', function() { + it('should include the outerHTML of the element', function() { + fixture.innerHTML = '
Hello!
'; + + var result = new DqElement(fixture.firstChild); + assert.equal(result.source, fixture.firstChild.outerHTML); + }); + + it('should work with SVG elements', function() { + fixture.innerHTML = ''; + + var result = new DqElement(fixture.firstChild); + assert.isString(result.source); + }); + it('should work with MathML', function() { + fixture.innerHTML = + 'x2'; + + var result = new DqElement(fixture.firstChild); + assert.isString(result.source); + }); + + it('should truncate large elements', function() { + var div = '
'; + for (var i = 0; i < 300; i++) { + div += i; + } + div += '
'; + fixture.innerHTML = div; + + var result = new DqElement(fixture.firstChild); + assert.equal(result.source.length, '
'.length); + }); + + it('should use spec object over passed element', function() { + fixture.innerHTML = '
Hello!
'; + var result = new DqElement( + fixture.firstChild, + {}, + { + source: 'woot' + } + ); + assert.equal(result.source, 'woot'); + }); + + it('should return null if audit.noHtml is set', function() { + axe.configure({ noHtml: true }); + fixture.innerHTML = '
Hello!
'; + var result = new DqElement(fixture.firstChild); + assert.isNull(result.source); + }); + + it('should not use spec object over passed element if audit.noHtml is set', function() { + axe.configure({ noHtml: true }); + fixture.innerHTML = '
Hello!
'; + var result = new DqElement( + fixture.firstChild, + {}, + { + source: 'woot' + } + ); + assert.isNull(result.source); + }); + }); + + describe('selector', function() { + it('should prefer selector from spec object', function() { + fixture.innerHTML = '
Hello!
'; + var result = new DqElement( + fixture.firstChild, + {}, + { + selector: 'woot' + } + ); + assert.equal(result.selector, 'woot'); + }); + }); + + describe('ancestry', function() { + it('should prefer selector from spec object', function() { + fixture.innerHTML = '
Hello!
'; + var result = new DqElement( + fixture.firstChild, + {}, + { + ancestry: 'woot' + } + ); + assert.equal(result.ancestry, 'woot'); + }); + }); + + describe('xpath', function() { + it('should prefer selector from spec object', function() { + fixture.innerHTML = '
Hello!
'; + var result = new DqElement( + fixture.firstChild, + {}, + { + xpath: 'woot' + } + ); + assert.equal(result.xpath, 'woot'); + }); + }); + + describe('absolutePaths', function() { + it('creates a path all the way to root', function() { + fixtureSetup('
Hello!
'); + + var result = new DqElement(fixture.firstChild, { + absolutePaths: true + }); + assert.include(result.selector[0], 'html > '); + assert.include(result.selector[0], '#fixture > '); + assert.include(result.selector[0], '#foo'); + }); + }); + + describe('toJSON', function() { + it('should only stringify selector and source', function() { + var expected = { + selector: 'foo > bar > joe', + source: '', + xpath: '/foo/bar/joe', + ancestry: 'foo > bar > joe' + }; + var result = new DqElement('joe', {}, expected); + + assert.deepEqual(JSON.stringify(result), JSON.stringify(expected)); + }); + }); + + describe('fromFrame', function() { + var dqMain, dqIframe; + beforeEach(function() { + var main = document.createElement('main'); + main.id = 'main'; + dqMain = new DqElement( + main, + {}, + { + selector: ['#main'], + ancestry: ['html > body > main'], + xpath: ['/main'] + } + ); + + var iframe = document.createElement('iframe'); + iframe.id = 'iframe'; + dqIframe = new DqElement( + iframe, + {}, + { + selector: ['#iframe'], + ancestry: ['html > body > iframe'], + xpath: ['/iframe'] + } + ); + }); + + it('returns a new DqElement', function() { + assert.instanceOf(DqElement.fromFrame(dqMain, {}, dqIframe), DqElement); + }); + + it('sets options for DqElement', function() { + var options = { absolutePaths: true }; + var dqElm = DqElement.fromFrame(dqMain, options, dqIframe); + assert.isTrue(dqElm._options.toRoot); + }); + + it('merges node and frame selectors', function() { + var dqElm = DqElement.fromFrame(dqMain, {}, dqIframe); + assert.deepEqual(dqElm.selector, [ + dqIframe.selector[0], + dqMain.selector[0] + ]); + assert.deepEqual(dqElm.ancestry, [ + dqIframe.ancestry[0], + dqMain.ancestry[0] + ]); + assert.deepEqual(dqElm.xpath, [dqIframe.xpath[0], dqMain.xpath[0]]); + }); + }); }); diff --git a/test/integration/full/configure-options/configure-options.js b/test/integration/full/configure-options/configure-options.js index 44ec4fbdf9..0ab804655a 100644 --- a/test/integration/full/configure-options/configure-options.js +++ b/test/integration/full/configure-options/configure-options.js @@ -1,16 +1,18 @@ describe('Configure Options', function() { 'use strict'; - afterEach(function() { - axe.reset(); - }); + var target = document.querySelector('#target'); + + afterEach(function() { + axe.reset(); + target.innerHTML = ''; + }); - describe('Check', function() { - var target = document.querySelector('#target'); - describe('aria-allowed-attr', function() { - it('should allow an attribute supplied in options', function(done) { - target.setAttribute('role', 'separator'); - target.setAttribute('aria-valuenow', '0'); + describe('Check', function() { + describe('aria-allowed-attr', function() { + it('should allow an attribute supplied in options', function(done) { + target.setAttribute('role', 'separator'); + target.setAttribute('aria-valuenow', '0'); axe.configure({ checks: [ @@ -150,11 +152,122 @@ describe('Configure Options', function() { assert.lengthOf(results.passes, 1, 'passes'); assert.equal(results.passes[0].id, 'html-has-lang'); - assert.lengthOf(results.violations, 0, 'violations'); - assert.lengthOf(results.incomplete, 0, 'incomplete'); - assert.lengthOf(results.inapplicable, 0, 'inapplicable'); - done(); - }); - }); - }); + assert.lengthOf(results.violations, 0, 'violations'); + assert.lengthOf(results.incomplete, 0, 'incomplete'); + assert.lengthOf(results.inapplicable, 0, 'inapplicable'); + done(); + }); + }); + }); + + describe('noHtml', function() { + it('prevents html property on nodes', function(done) { + target.setAttribute('role', 'slider'); + axe.configure({ + noHtml: true, + checks: [ + { + id: 'aria-required-attr', + options: { slider: ['aria-snuggles'] } + } + ] + }); + axe.run( + '#target', + { + runOnly: { + type: 'rule', + values: ['aria-required-attr'] + } + }, + function(error, results) { + try { + assert.isNull(results.violations[0].nodes[0].html); + done(); + } catch (e) { + done(e); + } + } + ); + }); + + it('prevents html property on nodes from iframes', function(done) { + axe.configure({ + noHtml: true, + rules: [ + { + id: 'div#target', + // purposefully don't match so the first result is from + // the iframe + selector: 'foo' + } + ] + }); + + var iframe = document.createElement('iframe'); + iframe.src = '/test/mock/frames/context.html'; + iframe.onload = function() { + axe.run( + '#target', + { + runOnly: { + type: 'rule', + values: ['div#target'] + } + }, + function(error, results) { + try { + assert.deepEqual(results.passes[0].nodes[0].target, [ + 'iframe', + '#target' + ]); + assert.isNull(results.passes[0].nodes[0].html); + done(); + } catch (e) { + done(e); + } + } + ); + }; + target.appendChild(iframe); + }); + + it('prevents html property in postMesage', function(done) { + axe.configure({ + noHtml: true, + rules: [ + { + id: 'div#target', + // purposefully don't match so the first result is from + // the iframe + selector: 'foo' + } + ] + }); + + var iframe = document.createElement('iframe'); + iframe.src = '/test/mock/frames/noHtml-config.html'; + iframe.onload = function() { + axe.run('#target', { + runOnly: { + type: 'rule', + values: ['div#target'] + } + }); + }; + target.appendChild(iframe); + + window.addEventListener('message', function(e) { + var data = JSON.parse(e.data); + if (Array.isArray(data.message)) { + try { + assert.isNull(data.message[0].nodes[0].node.source); + done(); + } catch (e) { + done(e); + } + } + }); + }); + }); }); diff --git a/test/mock/frames/noHtml-config.html b/test/mock/frames/noHtml-config.html new file mode 100644 index 0000000000..b6fe994736 --- /dev/null +++ b/test/mock/frames/noHtml-config.html @@ -0,0 +1,52 @@ + + + + Context Fixture + + +
+
+
+
+ + + +