Skip to content

Commit

Permalink
Axe-core v3.1.2 update
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew Lee committed Sep 27, 2018
1 parent 750c2e3 commit b6ccb80
Show file tree
Hide file tree
Showing 12 changed files with 222 additions and 61 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ language: node_js
node_js:
# we recommend testing addons with the same minimum supported node version as Ember CLI
# so that your addon works for all apps
- "4"
- "6"

sudo: required
dist: trusty
Expand Down
41 changes: 41 additions & 0 deletions addon/services/concurrent-axe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* global axe */
import Service from '@ember/service';
import { next } from '@ember/runloop';

export default Service.extend({
init() {
this._super(...arguments);
this._timer = null;
this._queue = [];
},

/**
* Axe v3 contains a concurrency issue which breaks the component auditing feature.
* This service defers axe.run calls on onto the next loop so that concurrent
* axe executions do not occur.
*
* @see(https://github.com/dequelabs/axe-core/issues/1041)
* @public
* @param {HTMLElement} element axe context
* @param {Object} options axe configuration options
* @param {Function} callback axe audit callback
* @return {Void}
*/
run(element, options, callback) {
if (this._timer) {
this._queue.push(arguments);
} else {
this._timer = next(() => {
if (element && element.parentNode) {
axe.run(element, options, callback);
}

this._timer = null;

if (this._queue.length) {
this.run(...this._queue.shift());
}
});
}
}
});
17 changes: 15 additions & 2 deletions app/instance-initializers/axe-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ENV from '../config/environment';
import isBackgroundReplacedElement from 'ember-a11y-testing/utils/is-background-replaced-element';
import formatViolation from 'ember-a11y-testing/utils/format-violation';
import violationsHelper from 'ember-a11y-testing/utils/violations-helper';
import { inject as service } from '@ember/service';

const VIOLATION_CLASS__LEVEL_1 = 'axe-violation--level-1';
const VIOLATION_CLASS__LEVEL_2 = 'axe-violation--level-2';
Expand Down Expand Up @@ -54,6 +55,15 @@ export function initialize() {
let turnAuditOff = configuredTurnAuditOff || false;

Component.reopen({
/**
* An Ember service which resolves concurrent axe.run v3 issue.
*
* @public
* @type {Object}
* @see(https://github.com/dequelabs/axe-core/issues/1041)
*/
concurrentAxe: service('concurrent-axe'),

/**
* An optional callback to process the results from the a11yCheck.
* Defaults to `undefined` if not set in the application's configuration.
Expand Down Expand Up @@ -138,12 +148,15 @@ export function initialize() {
*/
audit() {
if (this.get('tagName') !== '') {

axe.a11yCheck(this.$(), this.axeOptions, (results) => {
this.get('concurrentAxe').run(this.element, this.axeOptions, (error, results) => {
if (this.get('isDestroyed')) {
return;
}

if (error) {
throw error;
}

const violations = results.violations;
const violationClasses = this.get('violationClasses') || [];
const visualNoiseLevel = this.get('visualNoiseLevel');
Expand Down
1 change: 1 addition & 0 deletions app/services/concurrent-axe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'ember-a11y-testing/services/concurrent-axe';
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"test": "ember try:each"
},
"dependencies": {
"axe-core": "^2.6.1",
"axe-core": "^3.1.2",
"broccoli-funnel": "^2.0.1",
"ember-cli-babel": "^6.8.2",
"ember-cli-version-checker": "^2.1.0",
Expand Down
2 changes: 1 addition & 1 deletion tests/acceptance/a11y-audit-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ test('a11yAudit can accept an options hash as a single argument', function(asser
a11yAudit({
runOnly: {
type: "rule",
values: []
values: ["accesskeys"]
}
});

Expand Down
3 changes: 1 addition & 2 deletions tests/dummy/app/components/x-text-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ import Component from '@ember/component';


export default Component.extend({
classNames: ['c-text-input'],
tagName: 'input',
classNames: ['c-text-input']
});
2 changes: 1 addition & 1 deletion tests/dummy/app/templates/components/x-text-input.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<input
class="c-text-input__input"
id={{id}}
id={{inputId}}
type="text"
placeholder={{placeholder}}
value={{value}}>
16 changes: 8 additions & 8 deletions tests/dummy/app/templates/violations.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
value=3
name="noise-level"
class="c-radio-track__item-input"
radioId="noise-level-3"
id="noise-level-3"
changed="updateCurrentNoiseLevel"
}}
<span class="c-radio-track__item-node">Level 3</span>
Expand All @@ -74,7 +74,7 @@
<ul class="p-violations__grid u-fill-width u-relative">

{{! Empty button }}
{{#violations-grid-item class="p-violations__grid-item" title="Button without title"}}
{{#violations-grid-item class="p-violations__grid-item" title="Button without title" turnAuditOff=true}}

{{x-button
id="violations__empty-button"
Expand All @@ -85,12 +85,12 @@
{{/violations-grid-item}}

{{! Labeless text input }}
{{#violations-grid-item class="p-violations__grid-item" title="Input without label"}}
{{x-text-input id="violations__labeless-input" data-test-selector="labeless-text-input" visualNoiseLevel=model.currentNoiseLevel}}
{{#violations-grid-item class="p-violations__grid-item" title="Input without label" turnAuditOff=true}}
{{x-text-input inputId="violations__labeless-input" data-test-selector="labeless-text-input" visualNoiseLevel=model.currentNoiseLevel}}
{{/violations-grid-item}}

{{! Poorly Contrasting Text color }}
{{#violations-grid-item class="p-violations__grid-item" title="Poorly Contrasting Text Color"}}
{{#violations-grid-item class="p-violations__grid-item" title="Poorly Contrasting Text Color" turnAuditOff=true}}
{{#x-paragraph
class="p-violations__grid-item-content--low-contrast-text"
data-test-selector="labeless-text-input"
Expand All @@ -101,19 +101,19 @@
{{/violations-grid-item}}

{{! Usage of the <blink> element }}
{{#violations-grid-item class="p-violations__grid-item" title="Non-standard HTML elements"}}
{{#violations-grid-item class="p-violations__grid-item" title="Non-standard HTML elements" turnAuditOff=true}}
{{#x-paragraph id="violations__non-standard-html" data-test-selector="paragraph-with-blink-tag" visualNoiseLevel=model.currentNoiseLevel}}
Friends don't let friends use <code>&lt;blink&gt; tags</code>, <blink>like this one!</blink>
{{/x-paragraph}}
{{/violations-grid-item}}

{{! <img> tags without `alt` text }}
{{#violations-grid-item class="p-violations__grid-item" title="<img> Tags without `alt` attributes"}}
{{#violations-grid-item class="p-violations__grid-item" title="<img> Tags without `alt` attributes" turnAuditOff=true}}
{{x-image class="u-fill" id="violations__img-without-alt" src="assets/img/empire-state-building-moonlight.png" visualNoiseLevel=model.currentNoiseLevel}}
{{/violations-grid-item}}

{{! Related radio elements without a `<radiogroup>` }}
{{#violations-grid-item class="p-violations__grid-item" title="Ungrouped radio inputs with a shared name"}}
{{#violations-grid-item class="p-violations__grid-item" title="Ungrouped radio inputs with a shared name" turnAuditOff=true}}

{{!--
💡 PRO TIP: In lieu of a <radiogroup>, a containing <fieldset>
Expand Down
118 changes: 76 additions & 42 deletions tests/integration/instance-initializers/axe-component-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from 'dummy/instance-initializers/violations-helper';
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import wait from 'ember-test-helpers/wait';

const {
Logger
Expand Down Expand Up @@ -41,8 +42,8 @@ function setupDOMNode(id = ID_TEST_DOM_NODE, tagName = 'div') {
}

function stubA11yCheck(sandbox, callbackPayload) {
sandbox.stub(axe, 'a11yCheck').callsFake(function (el, options, callback) {
callback(callbackPayload);
sandbox.stub(axe, 'run').callsFake(function(el, options, callback) {
callback(undefined, callbackPayload);
});
}

Expand All @@ -60,6 +61,26 @@ function stubViolationOnDOMNode(sandbox, selector) {
});
}

function setupVisualNoiseLevel(visualNoiseLevel) {
initializeViolationsHelper();
stubViolationOnDOMNode(sandbox, `#${ID_TEST_DOM_NODE}`);
const dummyDOMNode = setupDOMNode(ID_TEST_DOM_NODE);

this.set('visualNoiseLevel', visualNoiseLevel);
this.render(hbs`{{axe-component visualNoiseLevel=visualNoiseLevel}}`);

return dummyDOMNode;
}

function assertVisualNoiseLevel(assert, visualNoiseLevel, dummyDOMNode) {
[1, 2, 3].forEach((_noiseLevel) => {
const assertFunc = (_noiseLevel === visualNoiseLevel) ? 'ok' : 'notOk';
assert[assertFunc](dummyDOMNode.classList.contains(VIOLATION_CLASS_MAP[`LEVEL_${_noiseLevel}`], `assert ${assertFunc} for level ${_noiseLevel}`));
});

dummyDOMNode.remove();
}

// We use a component integration test to verify the behavior of axe-component
// instance initializer since we are concerned with component behavior.
moduleForComponent('component:axe-component', 'Integration | Instance Initializer | axe-component', {
Expand Down Expand Up @@ -148,7 +169,9 @@ test('audit should log any violations found', function(assert) {
const logSpy = sandbox.spy(Logger, 'error');
this.render(hbs`{{#axe-component}}{{content}}{{/axe-component}}`);

assert.ok(logSpy.calledOnce);
return wait().then(() => {
assert.ok(logSpy.calledOnce);
});
});

test('audit should log any violations found if no nodes are found', function(assert) {
Expand All @@ -166,8 +189,7 @@ test('audit should log any violations found if no nodes are found', function(ass

const logSpy = sandbox.spy(Logger, 'error');
this.render(hbs`{{#axe-component}}{{content}}{{/axe-component}}`);

assert.ok(logSpy.calledOnce);
return wait().then(() => assert.ok(logSpy.calledOnce));
});

test('audit should do nothing if no violations found', function(assert) {
Expand All @@ -193,50 +215,56 @@ test('axeCallback receives the results of the audit', function(assert) {
this.set('axeCallbackSpy', axeCallbackSpy);
this.render(hbs`{{#axe-component axeCallback=axeCallbackSpy}}{{content}}{{/axe-component}}`);

assert.ok(axeCallbackSpy.calledOnce);
assert.ok(axeCallbackSpy.calledWith(results));
return wait().then(() => {
assert.ok(axeCallbackSpy.calledOnce);
assert.ok(axeCallbackSpy.calledWith(results));
});
});

test('axeCallback throws an error if it is not a function', function(assert) {
const results = { violations: [] };

sandbox.stub(axe, 'a11yCheck').callsFake(function (el, options, callback) {
assert.throws(() => callback(results) , /axeCallback should be a function./);
const a11yCheckStub = sandbox.stub(axe, 'run').callsFake(function(el, options, callback) {
assert.throws(() => callback(undefined, results), /axeCallback should be a function./);
});

this.render(hbs`{{#axe-component axeCallback='axeCallbackSpy'}}{{content}}{{/axe-component}}`);

return wait().then(() => assert.ok(a11yCheckStub.calledOnce));
});

/* Component.axeOptions */

test('axeOptions are passed in as the second param to a11yCheck', function(assert) {
const a11yCheckStub = sandbox.stub(axe, 'a11yCheck');
const a11yCheckStub = sandbox.stub(axe, 'run');
const axeOptions = { test: 'test' };
this.set('axeOptions', axeOptions);
this.render(hbs`{{axe-component class="component" axeOptions=axeOptions}}`);

assert.ok(a11yCheckStub.calledOnce);
assert.ok(a11yCheckStub.calledWith(sinon.match.any, axeOptions));
return wait().then(() => {
assert.ok(a11yCheckStub.calledOnce);
assert.ok(a11yCheckStub.calledWith(sinon.match.any, axeOptions));
});
});

test('#violationClasses is computed from the current `visualNoiseLevel`', function(assert) {
initializeViolationsHelper();
test('#violationClass is computed from `visualNoiseLevel` 1', function(assert) {
const visualNoiseLevel = 1;
const dummyDOMNode = setupVisualNoiseLevel.call(this, visualNoiseLevel);

stubViolationOnDOMNode(sandbox, `#${ID_TEST_DOM_NODE}`);
return wait().then(() => assertVisualNoiseLevel(assert, visualNoiseLevel, dummyDOMNode));
});

const dummyDOMNode = setupDOMNode(ID_TEST_DOM_NODE);
test('#violationClass is computed from `visualNoiseLevel` 2', function(assert) {
const visualNoiseLevel = 2;
const dummyDOMNode = setupVisualNoiseLevel.call(this, visualNoiseLevel);

[1, 2, 3].forEach((currentNoiseLevel) => {
this.set('visualNoiseLevel', currentNoiseLevel);
this.render(hbs`{{axe-component visualNoiseLevel=visualNoiseLevel}}`);
return wait().then(() => assertVisualNoiseLevel(assert, visualNoiseLevel, dummyDOMNode));
});

[1, 2, 3].forEach((_noiseLevel) => {
const assertFunc = (_noiseLevel === currentNoiseLevel) ? 'ok' : 'notOk';
assert[assertFunc](dummyDOMNode.classList.contains(VIOLATION_CLASS_MAP[`LEVEL_${_noiseLevel}`], `assert ${assertFunc} for level ${_noiseLevel}`));
});
});
test('#violationClass is computed from `visualNoiseLevel` 3', function(assert) {
const visualNoiseLevel = 3;
const dummyDOMNode = setupVisualNoiseLevel.call(this, visualNoiseLevel);

dummyDOMNode.remove();
return wait().then(() => assertVisualNoiseLevel(assert, visualNoiseLevel, dummyDOMNode));
});

test('`axeViolationClassNames` can be passed as a space-separated string (to aid template usage)', function (assert) {
Expand All @@ -249,9 +277,10 @@ test('`axeViolationClassNames` can be passed as a space-separated string (to aid
this.set('axeViolationClassNames', 'spark 🐋 foo ');
this.render(hbs`{{axe-component axeViolationClassNames=axeViolationClassNames}}`);

assert.deepEqual([].slice.call(dummyDOMNode.classList), ['spark', '🐋', 'foo']);

dummyDOMNode.remove();
return wait().then(() => {
assert.deepEqual([].slice.call(dummyDOMNode.classList), ['spark', '🐋', 'foo']);
dummyDOMNode.remove();
});
});

test('#violationClasses will always give precedence to a `axeViolationClassNames`, if it is set', function (assert) {
Expand All @@ -265,15 +294,17 @@ test('#violationClasses will always give precedence to a `axeViolationClassNames
this.set('axeViolationClassNames', axeViolationClassNames);
this.render(hbs`{{axe-component axeViolationClassNames=axeViolationClassNames}}`);

axeViolationClassNames.forEach(className => {
assert.ok(dummyDOMNode.classList.contains(className));
});
return wait().then(() => {
axeViolationClassNames.forEach(className => {
assert.ok(dummyDOMNode.classList.contains(className));
});

[1, 2, 3].forEach(noiseLevel => {
assert.notOk(dummyDOMNode.classList.contains(VIOLATION_CLASS_MAP[`LEVEL_${noiseLevel}`]));
});
[1, 2, 3].forEach(noiseLevel => {
assert.notOk(dummyDOMNode.classList.contains(VIOLATION_CLASS_MAP[`LEVEL_${noiseLevel}`]));
});

dummyDOMNode.remove();
dummyDOMNode.remove();
});
});

test('using default class names for violations when no `axeViolationClassNames` is provided', function (assert) {
Expand All @@ -285,9 +316,10 @@ test('using default class names for violations when no `axeViolationClassNames`

this.render(hbs`{{axe-component}}`);

assert.ok(dummyDOMNode.classList.contains(VIOLATION_CLASS_MAP.LEVEL_1));

dummyDOMNode.remove();
return wait().then(() => {
assert.ok(dummyDOMNode.classList.contains(VIOLATION_CLASS_MAP.LEVEL_1));
dummyDOMNode.remove();
});
});

test(`smartly detects replaced elements and applies a special \`border-box\` style instead
Expand All @@ -302,8 +334,10 @@ of the styles from the current setting`, function(assert) {
this.set('axeViolationClassNames', [customViolationClass]);
this.render(hbs`{{axe-component axeViolationClassNames=axeViolationClassNames}}`);

assert.ok(dummyDOMNode.classList.contains(VIOLATION_CLASS_MAP.REPLACED_ELEMENT));
assert.notOk(dummyDOMNode.classList.contains(customViolationClass));
return wait().then(() => {
assert.ok(dummyDOMNode.classList.contains(VIOLATION_CLASS_MAP.REPLACED_ELEMENT));
assert.notOk(dummyDOMNode.classList.contains(customViolationClass));

dummyDOMNode.remove();
dummyDOMNode.remove();
})
});
Loading

0 comments on commit b6ccb80

Please sign in to comment.