From 4bf6b7be787545e30fbc36d13ee4cb63ab4f4274 Mon Sep 17 00:00:00 2001 From: Steve Calvert Date: Sat, 22 Aug 2020 14:34:57 -0700 Subject: [PATCH] Refactoring parameter normalization --- addon-test-support/audit.ts | 134 ++++++++++++++++++++++-------------- tests/unit/audit-test.ts | 94 +++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 52 deletions(-) create mode 100644 tests/unit/audit-test.ts diff --git a/addon-test-support/audit.ts b/addon-test-support/audit.ts index 755afb12..e1531702 100644 --- a/addon-test-support/audit.ts +++ b/addon-test-support/audit.ts @@ -1,12 +1,31 @@ import { assert } from '@ember/debug'; import RSVP from 'rsvp'; -import { run, AxeResults, RunOptions, ElementContext } from 'axe-core'; +import { + run, + AxeResults, + RunOptions, + ElementContext, + ContextObject, +} from 'axe-core'; import config from 'ember-get-config'; import formatViolation from 'ember-a11y-testing/utils/format-violation'; import violationsHelper from 'ember-a11y-testing/utils/violations-helper'; import { mark, markEndAndMeasure } from './utils'; -type MaybeContextObject = ElementContext | RunOptions | undefined; +type MaybeElementContext = ElementContext | RunOptions | undefined; + +let _configName = 'ember-a11y-testing'; + +/** + * Test only function used to mimic the behavior of when there's no + * default config + * + * @param configName + * @private + */ +export function _setConfigName(configName = 'ember-a11y-testing') { + _configName = configName; +} /** * Processes the results of calling axe.a11yCheck. If there are any @@ -39,28 +58,62 @@ function processAxeResults(results: AxeResults) { } /** - * Determines if an object is a plain object (as opposed to a jQuery or other - * type of object). - * @param {Object} obj - * @return {Boolean} + * Validation function used to determine if we have the shape of an {ElementContext} object. + * + * Function mirrors what axe-core uses for internal param validation. + * https://github.com/dequelabs/axe-core/blob/d5b6931cba857a5c787d912ee56bdd973e3742d4/lib/core/public/run.js#L4 + * + * @param potential */ -function isPlainObj(obj: MaybeContextObject) { - return typeof obj == 'object' && obj.constructor == Object; +export function _isContext(potential: MaybeElementContext) { + 'use strict'; + switch (true) { + case typeof potential === 'string': + case Array.isArray(potential): + case self.Node && potential instanceof self.Node: + case self.NodeList && potential instanceof self.NodeList: + return true; + + case typeof potential !== 'object': + return false; + + case (potential).include !== undefined: + case (potential).exclude !== undefined: + return true; + + default: + return false; + } } /** - * Determines whether supplied object contains `include` and `exclude` axe - * context selector properties. This is necessary to distinguish an axe - * config object from a context selector object, after a single argument - * is supplied to `runA11yAudit`. - * @param {Object} obj - * @return {Boolean} + * Normalize the optional params of axe.run() + * + * Influenced by https://github.com/dequelabs/axe-core/blob/d5b6931cba857a5c787d912ee56bdd973e3742d4/lib/core/public/run.js#L35 + * + * @param elementContext + * @param runOptions */ -function isNotContextObject(obj: MaybeContextObject) { - return ( - !Object.prototype.hasOwnProperty.call(obj, 'include') && - !Object.prototype.hasOwnProperty.call(obj, 'exclude') - ); +export function _normalizeRunParams( + elementContext?: MaybeElementContext, + runOptions?: RunOptions | undefined +): [ElementContext, RunOptions] { + let context: ElementContext; + let options: RunOptions | undefined; + + if (!_isContext(elementContext)) { + options = elementContext; + context = '#ember-testing-container'; + } else { + context = elementContext; + options = runOptions; + } + + if (typeof options !== 'object') { + options = config[_configName]?.componentOptions?.axeOptions || {}; + } + + return [context, options!]; } /** @@ -72,50 +125,27 @@ function isNotContextObject(obj: MaybeContextObject) { * @private */ export default function a11yAudit( - contextSelector: - | ElementContext - | RunOptions - | undefined = '#ember-testing-container', + contextSelector: MaybeElementContext = '#ember-testing-container', axeOptions?: RunOptions | undefined ) { mark('a11y_audit_start'); - let context: ElementContext; - let options: RunOptions | undefined = axeOptions; - - // Support passing axeOptions as a single argument - if (arguments.length === 1) { - if (isPlainObj(contextSelector) && isNotContextObject(contextSelector)) { - context = '#ember-testing-container'; - options = contextSelector; - } else { - context = contextSelector; - } - } else { - context = contextSelector; - } - - if (!options) { - // Try load default config - let a11yConfig = config['ember-a11y-testing'] || {}; - let componentOptions = a11yConfig['componentOptions'] || {}; - options = componentOptions['axeOptions'] || {}; - } + let [context, options] = _normalizeRunParams(contextSelector, axeOptions); document.body.classList.add('axe-running'); - let auditPromise = new RSVP.Promise((resolve, reject) => { - run(context, options!, (error, result) => { + return new RSVP.Promise((resolve, reject) => { + run(context, options, (error, result) => { if (!error) { return resolve(result); } else { return reject(error); } }); - }); - - return auditPromise.then(processAxeResults).finally(() => { - document.body.classList.remove('axe-running'); - markEndAndMeasure('a11y_audit', 'a11y_audit_start', 'a11y_audit_end'); - }); + }) + .then(processAxeResults) + .finally(() => { + document.body.classList.remove('axe-running'); + markEndAndMeasure('a11y_audit', 'a11y_audit_start', 'a11y_audit_end'); + }); } diff --git a/tests/unit/audit-test.ts b/tests/unit/audit-test.ts new file mode 100644 index 00000000..7b562905 --- /dev/null +++ b/tests/unit/audit-test.ts @@ -0,0 +1,94 @@ +import { module, test } from 'qunit'; +import { + _normalizeRunParams, + _setConfigName, + _isContext, +} from 'ember-a11y-testing/test-support/audit'; + +module('audit', function () { + module('with no config', function (hooks) { + hooks.beforeEach(function () { + _setConfigName('foo'); + }); + + hooks.afterEach(function () { + _setConfigName(); + }); + + test('_normalizeRunParams returns defaults when no params provided', function (assert) { + let [context, options] = _normalizeRunParams(); + + assert.equal(context, '#ember-testing-container'); + assert.deepEqual(options, {}); + }); + + test('_normalizeRunParams returns default options when only string context provided', function (assert) { + let ctx = '#my-container'; + let [context, options] = _normalizeRunParams(ctx); + + assert.equal(context, '#my-container'); + assert.deepEqual(options, {}); + }); + + test('_normalizeRunParams returns default options when only Node context provided', function (assert) { + let ctx = document; + let [context, options] = _normalizeRunParams(ctx); + + assert.equal(context, document); + assert.deepEqual(options, {}); + }); + + test('_normalizeRunParams returns default options when only ElementContext provided', function (assert) { + let ctx = { include: ['me'] }; + let [context, options] = _normalizeRunParams(ctx); + + assert.equal(context, ctx); + assert.deepEqual(options, {}); + }); + + test('_normalizeRunParams returns defaults context when only options provided', function (assert) { + let opts = {}; + let [context, options] = _normalizeRunParams(opts); + + assert.equal(context, '#ember-testing-container'); + assert.deepEqual(options, opts); + }); + + test('_normalizeRunParams returns context and options when both provided', function (assert) { + let ctx = '#my-container'; + let opts = {}; + let [context, options] = _normalizeRunParams(ctx, opts); + + assert.equal(context, ctx); + assert.deepEqual(options, opts); + }); + }); + + module('with config', function () { + test('_normalizeRunParams returns defaults when no params provided', function (assert) { + let [context, options] = _normalizeRunParams(); + + assert.equal(context, '#ember-testing-container'); + assert.ok(Object.keys(options).length > 0); + }); + + test('_normalizeRunParams returns config options when only string context provided', function (assert) { + let ctx = '#my-container'; + let [context, options] = _normalizeRunParams(ctx); + + assert.equal(context, '#my-container'); + assert.ok(Object.keys(options).length > 0); + }); + }); + + test('_isContext', function (assert) { + assert.ok(_isContext('#foo')); + assert.ok(_isContext(document)); + assert.ok(_isContext({ include: [] })); + assert.ok(_isContext({ exclude: [] })); + assert.ok(_isContext({ include: [], exclude: [] })); + + assert.notOk(_isContext(undefined)); + assert.notOk(_isContext({})); + }); +});