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

Axe-core v3.1.2 update #109

Merged
merged 1 commit into from
Oct 1, 2018
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
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(() => {
drewlee marked this conversation as resolved.
Show resolved Hide resolved
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