Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop support for moduleForAcceptance #199

Merged
merged 3 commits into from
Aug 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions addon-test-support/audit-if.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import RSVP from 'rsvp';
import a11yAudit from './audit';
import utils from './utils';
import { ElementContext, RunOptions } from 'axe-core';

/**
* A method to return the value of queryParameter
Expand All @@ -23,9 +24,12 @@ function getUrlParameter(name: string) {
* @method a11yAuditIf
* @public
*/
export default function a11yAuditIf(...args: any[]) {
export default function a11yAuditIf(
contextSelector?: ElementContext | RunOptions | undefined,
axeOptions?: RunOptions | undefined
) {
if (getUrlParameter('enableA11yAudit') === 'true') {
return a11yAudit(...args);
return a11yAudit(contextSelector, axeOptions);
}

return RSVP.resolve(undefined, 'a11y audit not run');
Expand Down
154 changes: 84 additions & 70 deletions addon-test-support/audit.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
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
* violations, it throws an error and then logs them individually.
* @param {Object} results
* @return {Void}
*/
function a11yAuditCallback(results: AxeResults) {
function processAxeResults(results: AxeResults) {
let violations = results.violations;

if (violations.length) {
Expand All @@ -39,28 +58,62 @@ function a11yAuditCallback(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 (<ContextObject>potential).include !== undefined:
case (<ContextObject>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 = <RunOptions>elementContext;
context = '#ember-testing-container';
} else {
context = <ElementContext>elementContext;
options = runOptions;
}

if (typeof options !== 'object') {
options = config[_configName]?.componentOptions?.axeOptions || {};
}

return [context, options!];
}

/**
Expand All @@ -71,67 +124,28 @@ function isNotContextObject(obj: MaybeContextObject) {
* @method runA11yAudit
* @private
*/
function runA11yAudit(
contextSelector:
| ElementContext
| RunOptions
| undefined = '#ember-testing-container',
export default function a11yAudit(
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 = <RunOptions>contextSelector;
} else {
context = <ElementContext>contextSelector;
}
} else {
context = <ElementContext>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(a11yAuditCallback).finally(() => {
document.body.classList.remove('axe-running');
markEndAndMeasure('a11y_audit', 'a11y_audit_start', 'a11y_audit_end');
});
}

/**
* A wrapper method to run the async a11yAudit test helper if in an acceptance
* testing situation, but also supports being used in integration/unit test
* scenarios.
*
* @method a11yAudit
* @public
*/
export default function a11yAudit(...args: any[]) {
if ((<any>window).a11yAudit) {
return (<any>window).a11yAudit(...args);
}
scalvert marked this conversation as resolved.
Show resolved Hide resolved

return runA11yAudit(...args);
})
.then(processAxeResults)
scalvert marked this conversation as resolved.
Show resolved Hide resolved
.finally(() => {
document.body.classList.remove('axe-running');
markEndAndMeasure('a11y_audit', 'a11y_audit_start', 'a11y_audit_end');
});
}
94 changes: 94 additions & 0 deletions tests/unit/audit-test.ts
Original file line number Diff line number Diff line change
@@ -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({}));
});
});