diff --git a/app/instance-initializers/axe-component.js b/app/instance-initializers/axe-component.js index 4d3dcbea..2e4d702e 100644 --- a/app/instance-initializers/axe-component.js +++ b/app/instance-initializers/axe-component.js @@ -39,6 +39,17 @@ export function initialize(application) { */ turnAuditOff: false, + /** + * An array of classNames to add to the component when a violation occurs. + * If unspecified, the `axe-violation` class is used to apply our default + * styling + * + * @public + * @type {Array} + */ + axeViolationClassNames: ['axe-violation'], + + /** * Runs an accessibility audit on any render of the component. * @private @@ -59,7 +70,8 @@ export function initialize(application) { audit() { if (this.get('tagName') !== '') { axe.a11yCheck(this.$(), this.axeOptions, (results) => { - let violations = results.violations; + const violations = results.violations; + const violationClassNames = this.get('axeViolationClassNames'); for (let i = 0, l = violations.length; i < l; i++) { let violation = violations[i]; @@ -72,7 +84,7 @@ export function initialize(application) { let node = nodes[j]; if (node) { - Ember.$(node.target.join(','))[0].classList.add('axe-violation'); + Ember.$(node.target.join(','))[0].classList.add(...violationClassNames); } } } diff --git a/bower.json b/bower.json index 37749e17..0fe74e1b 100644 --- a/bower.json +++ b/bower.json @@ -5,7 +5,7 @@ "ember-cli-shims": "0.1.1", "ember-cli-test-loader": "0.2.2", "ember-qunit-notifications": "0.1.0", - "axe-core": "~1.0.1", + "axe-core": "~1.1.1", "sinonjs": "~1.14.1" } } diff --git a/content-for/head-footer.html b/content-for/head-footer.html index 8ef69e1e..61f3f7a9 100644 --- a/content-for/head-footer.html +++ b/content-for/head-footer.html @@ -1,5 +1,19 @@ diff --git a/index.js b/index.js index 0755c932..fabe5f07 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,7 @@ var ALLOWED_CONTENT_FOR = [ module.exports = { name: 'ember-a11y-testing', + /** * Includes axe-core in non-production builds. It includes the un-minified * version in case of a need to debug. diff --git a/package.json b/package.json index a561d2a0..da6aa798 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "testing" ], "dependencies": { + "bower": "1.7.9", "broccoli-funnel": "^1.0.1", "ember-cli-babel": "^5.1.6", "ember-cli-version-checker": "^1.1.6" diff --git a/tests/.jshintrc b/tests/.jshintrc index ea8b88f6..c3d1a718 100644 --- a/tests/.jshintrc +++ b/tests/.jshintrc @@ -19,6 +19,7 @@ "wait", "DS", "andThen", + "axe", "currentURL", "currentPath", "currentRouteName" diff --git a/tests/acceptance/auto-run-test.js b/tests/acceptance/auto-run-test.js index 42db68cb..b10f22fe 100644 --- a/tests/acceptance/auto-run-test.js +++ b/tests/acceptance/auto-run-test.js @@ -6,6 +6,10 @@ import sinon from 'sinon'; let application; let sandbox; +const SELECTORS = { + passingInput: '[data-test-selector="passing-input"]' +}; + module('Acceptance | auto-run', { beforeEach: function() { application = startApp(); @@ -39,7 +43,7 @@ test('should run the function whenever a render occurs', function(assert) { assert.equal(currentPath(), 'index'); }); - click('label'); + click(`${SELECTORS.passingInput} label`); andThen(() => { assert.ok(callbackStub.calledTwice); diff --git a/tests/acceptance/violations-test.js b/tests/acceptance/violations-test.js new file mode 100644 index 00000000..38a330ae --- /dev/null +++ b/tests/acceptance/violations-test.js @@ -0,0 +1,38 @@ +import Ember from 'ember'; +import { test } from 'qunit'; +import moduleForAcceptance from '../../tests/helpers/module-for-acceptance'; +import sinon from 'sinon'; + +let actual, expected, sandbox; + +const IDs = { + emptyButton: '#empty-button', + sloppyInput: '#sloppy-input' +}; + +const findBy = (items, key, val) => items.find(item => item[key] === val); + +moduleForAcceptance('Acceptance | violations', { + beforeEach() { + sandbox = sinon.sandbox.create(); + }, + + afterEach() { + sandbox.restore(); + } +}); + +test('marking DOM nodes with violations', function(assert) { + + sandbox.stub(axe.ember, 'a11yCheckCallback', function (results) { + + assert.equal(results.violations.length, 2); + + const buttonNameViolation = findBy(results.violations, 'id', 'button-name'); + assert.equal(buttonNameViolation.nodes[0].target[0], IDs.emptyButton); + + }); + + visit('/violations'); + +}); diff --git a/tests/dummy/app/components/_base-demo-component.js b/tests/dummy/app/components/_base-demo-component.js new file mode 100644 index 00000000..72b0d076 --- /dev/null +++ b/tests/dummy/app/components/_base-demo-component.js @@ -0,0 +1,7 @@ +import Ember from 'ember'; + +const { Component } = Ember; + +export default Component.extend({ + attributeBindings: ['data-test-selector'] +}); diff --git a/tests/dummy/app/components/empty-button.js b/tests/dummy/app/components/empty-button.js new file mode 100644 index 00000000..d2070d07 --- /dev/null +++ b/tests/dummy/app/components/empty-button.js @@ -0,0 +1,6 @@ +import BaseDemoComponent from './_base-demo-component'; + +export default BaseDemoComponent.extend({ + classNames: ['button'], + tagName: 'button' +}); diff --git a/tests/dummy/app/components/passing-component.js b/tests/dummy/app/components/passing-component.js index 10c85285..b6ae22a3 100644 --- a/tests/dummy/app/components/passing-component.js +++ b/tests/dummy/app/components/passing-component.js @@ -1,6 +1,6 @@ -import Ember from 'ember'; +import BaseDemoComponent from './_base-demo-component'; -export default Ember.Component.extend({ +export default BaseDemoComponent.extend({ actions: { toggle() { this.set('isFailing', true); diff --git a/tests/dummy/app/components/sloppy-input.js b/tests/dummy/app/components/sloppy-input.js new file mode 100644 index 00000000..eb9a3cb4 --- /dev/null +++ b/tests/dummy/app/components/sloppy-input.js @@ -0,0 +1,5 @@ +import BaseDemoComponent from './_base-demo-component'; + +export default BaseDemoComponent.extend({ + tagName: 'input', +}); diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js index 3bba78eb..9f99b137 100644 --- a/tests/dummy/app/router.js +++ b/tests/dummy/app/router.js @@ -6,6 +6,7 @@ const Router = Ember.Router.extend({ }); Router.map(function() { + this.route('violations'); }); export default Router; diff --git a/tests/dummy/app/routes/index.js b/tests/dummy/app/routes/index.js new file mode 100644 index 00000000..26d9f312 --- /dev/null +++ b/tests/dummy/app/routes/index.js @@ -0,0 +1,4 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ +}); diff --git a/tests/dummy/app/routes/violations.js b/tests/dummy/app/routes/violations.js new file mode 100644 index 00000000..26d9f312 --- /dev/null +++ b/tests/dummy/app/routes/violations.js @@ -0,0 +1,4 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ +}); diff --git a/tests/dummy/app/styles/app.css b/tests/dummy/app/styles/app.css index e69de29b..77189728 100644 --- a/tests/dummy/app/styles/app.css +++ b/tests/dummy/app/styles/app.css @@ -0,0 +1,10 @@ +.button { + background-color: aqua; + border-radius: 0.25em; + border: none; + min-height: 1.5rem; + height: 1.5rem; + + width: 2.2rem; + min-width: 2.2rem; +} diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs index d6a7f4ef..f8bc38e7 100644 --- a/tests/dummy/app/templates/application.hbs +++ b/tests/dummy/app/templates/application.hbs @@ -1,3 +1,3 @@ -

Welcome to Ember.js

+

Welcome to Ember

-{{passing-component}} +{{outlet}} diff --git a/tests/dummy/app/templates/components/empty-button.hbs b/tests/dummy/app/templates/components/empty-button.hbs new file mode 100644 index 00000000..889d9eea --- /dev/null +++ b/tests/dummy/app/templates/components/empty-button.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/tests/dummy/app/templates/components/sloppy-input.hbs b/tests/dummy/app/templates/components/sloppy-input.hbs new file mode 100644 index 00000000..889d9eea --- /dev/null +++ b/tests/dummy/app/templates/components/sloppy-input.hbs @@ -0,0 +1 @@ +{{yield}} diff --git a/tests/dummy/app/templates/index.hbs b/tests/dummy/app/templates/index.hbs new file mode 100644 index 00000000..f7f52238 --- /dev/null +++ b/tests/dummy/app/templates/index.hbs @@ -0,0 +1,2 @@ +{{passing-component data-test-selector="passing-input"}} +{{outlet}} diff --git a/tests/dummy/app/templates/violations.hbs b/tests/dummy/app/templates/violations.hbs new file mode 100644 index 00000000..a46a7a12 --- /dev/null +++ b/tests/dummy/app/templates/violations.hbs @@ -0,0 +1,3 @@ +{{empty-button id="empty-button" data-test-selector="empty-button"}} + +{{sloppy-input id="sloppy-input" data-test-selector="sloppy-input"}} diff --git a/tests/unit/instance-initializers/axe-component-test.js b/tests/unit/instance-initializers/axe-component-test.js index 56168af3..02925896 100644 --- a/tests/unit/instance-initializers/axe-component-test.js +++ b/tests/unit/instance-initializers/axe-component-test.js @@ -3,13 +3,15 @@ import Ember from 'ember'; import { initialize } from 'dummy/instance-initializers/axe-component'; import { module, test, skip } from 'qunit'; +const { Application, Component, Logger, run } = Ember; + let application; let sandbox; module('Unit | Instance Initializer | axe-component', { beforeEach() { - Ember.run(() => { - application = Ember.Application.create({ + run(() => { + application = Application.create({ rootElement: '#ember-testing' }); application.deferReadiness(); @@ -25,11 +27,11 @@ module('Unit | Instance Initializer | axe-component', { /* Basic Behavior */ -test('initializer should not re-open Ember.Component more than once', function(assert) { +test('initializer should not re-open Component more than once', function(assert) { // Depending on if the initializer has already ran, we will either expect the // reopen method to be called once or not at all. - let assertMethod = Ember.Component.prototype.audit ? 'notCalled' : 'calledOnce'; - let reopenSpy = sandbox.spy(Ember.Component, 'reopen'); + let assertMethod = Component.prototype.audit ? 'notCalled' : 'calledOnce'; + let reopenSpy = sandbox.spy(Component, 'reopen'); initialize(application); initialize(application); @@ -40,19 +42,19 @@ test('initializer should not re-open Ember.Component more than once', function(a test('audit is run on didRender when not in testing mode', function(assert) { initialize(application); - let component = Ember.Component.create({}); + let component = Component.create({}); let auditSpy = sandbox.spy(component, 'audit'); // In order for the audit to run, we have to act like we're not in testing Ember.testing = false; - Ember.run(() => component.appendTo('#ember-testing')); + run(() => component.appendTo('#ember-testing')); assert.ok(auditSpy.calledOnce); - Ember.run(() => component.trigger('didRender')); + run(() => component.trigger('didRender')); assert.ok(auditSpy.calledTwice); - Ember.run(() => component.destroy()); + run(() => component.destroy()); // Turn testing mode back on to ensure validity of other tests Ember.testing = true; @@ -61,36 +63,36 @@ test('audit is run on didRender when not in testing mode', function(assert) { test('audit is not run on didRender when in testing mode', function(assert) { initialize(application); - let component = Ember.Component.create({}); + let component = Component.create({}); let auditSpy = sandbox.spy(component, 'audit'); - Ember.run(() => component.appendTo('#ember-testing')); + run(() => component.appendTo('#ember-testing')); assert.ok(auditSpy.notCalled); - Ember.run(() => component.destroy()); + run(() => component.destroy()); }); -/* Ember.Component.turnAuditOff */ +/* Component.turnAuditOff */ test('turnAuditOff prevents audit from running on didRender', function(assert) { initialize(application); - let component = Ember.Component.create({ turnAuditOff: true }); + let component = Component.create({ turnAuditOff: true }); let auditSpy = sandbox.spy(component, 'audit'); // In order for the audit to run, we have to act like we're not in testing Ember.testing = false; - Ember.run(() => component.appendTo('#ember-testing')); + run(() => component.appendTo('#ember-testing')); assert.ok(auditSpy.notCalled); - Ember.run(() => component.destroy()); + run(() => component.destroy()); // Turn testing mode back on to ensure validity of other tests Ember.testing = true; }); -/* Ember.Component.audit */ +/* Component.audit */ test('audit should log any violations found', function(assert) { let a11yCheckStub = sandbox.stub(axe, 'a11yCheck', function(el, options, callback) { @@ -102,17 +104,14 @@ test('audit should log any violations found', function(assert) { }); }); - let logSpy = sandbox.spy(Ember.Logger, 'error'); + let logSpy = sandbox.spy(Logger, 'error'); - let component = Ember.Component.create({}); + let component = Component.create({}); component.audit(); assert.ok(logSpy.calledOnce); }); -skip('audit should mark the DOM nodes of any violations', function(assert) { - -}); test('audit should do nothing if no violations found', function(assert) { let a11yCheckStub = sandbox.stub(axe, 'a11yCheck', function(el, options, callback) { @@ -121,15 +120,15 @@ test('audit should do nothing if no violations found', function(assert) { }); }); - let logSpy = sandbox.spy(Ember.Logger, 'error'); + let logSpy = sandbox.spy(Logger, 'error'); - let component = Ember.Component.create({}); + let component = Component.create({}); component.audit(); assert.ok(logSpy.notCalled); }); -/* Ember.Component.axeCallback */ +/* Component.axeCallback */ test('axeCallback receives the results of the audit', function(assert) { let results = { violations: [] }; @@ -138,7 +137,7 @@ test('axeCallback receives the results of the audit', function(assert) { }); let axeCallbackSpy = sandbox.spy(); - let component = Ember.Component.create({ + let component = Component.create({ axeCallback: axeCallbackSpy }); @@ -154,22 +153,83 @@ test('axeCallback throws an error if it is not a function', function(assert) { callback(results); }); - let component = Ember.Component.create({ + let component = Component.create({ axeCallback: 'axeCallbackSpy' }); assert.throws(() => component.audit(), 'axeCallback should be a function.'); }); -/* Ember.Component.axeOptions */ +/* Component.axeOptions */ test('axeOptions are passed in as the second param to a11yCheck', function(assert) { let a11yCheckStub = sandbox.stub(axe, 'a11yCheck'); let axeOptions = { test: 'test' }; - let component = Ember.Component.create({ axeOptions }); + let component = Component.create({ axeOptions }); component.audit(); assert.ok(a11yCheckStub.calledOnce); assert.ok(a11yCheckStub.calledWith(component.$(), axeOptions)); }); + +test('custom classNames are set on the violating elmenet if they are defined', function (assert) { + const dummyDOMNodeID = 'sign-up-button'; + const dummyDOMNodeClass = 'icon-left-shark'; + const dummyDOMNode = document.createElement('div'); + + const component = Component.create({ + axeViolationClassNames: [dummyDOMNodeClass] + }); + + dummyDOMNode.setAttribute('id', dummyDOMNodeID); + document.body.appendChild(dummyDOMNode); + + // run(() => component.appendTo('#ember-testing')); + let a11yCheckStub = sandbox.stub(axe, 'a11yCheck', function(el, options, callback) { + callback({ + violations: [{ + name: 'test', + nodes: [ + { target: [`#${dummyDOMNodeID}`] } + ] + }] + }); + }); + + component.audit(); + + assert.ok(dummyDOMNode.classList.contains(dummyDOMNodeClass)); + assert.notOk(dummyDOMNode.classList.contains('axe-violation')); + + run(() => dummyDOMNode.remove()); +}); + +test(`the component defaults to setting the \`axe-violation\` class on + the element if no custom class names are set`, function (assert) { + + const dummyDOMNodeID = 'sign-up-button'; + const dummyDOMNode = document.createElement('div'); + const component = Component.create(); + + dummyDOMNode.setAttribute('id', dummyDOMNodeID); + document.body.appendChild(dummyDOMNode); + + // run(() => component.appendTo('#ember-testing')); + let a11yCheckStub = sandbox.stub(axe, 'a11yCheck', function(el, options, callback) { + callback({ + violations: [{ + name: 'test', + nodes: [ + { target: [`#${dummyDOMNodeID}`] } + ] + }] + }); + }); + + component.audit(); + + assert.ok(dummyDOMNode.classList.contains('axe-violation')); + + run(() => dummyDOMNode.remove()); +});