axe-core
Version:
Accessibility engine for automated Web UI testing
694 lines (631 loc) • 16.4 kB
JavaScript
/*global runRules */
describe('runRules', function () {
'use strict';
var ver = axe.version.substring(0, axe.version.lastIndexOf('.'));
function iframeReady(src, context, id, cb) {
var i = document.createElement('iframe');
i.addEventListener('load', function () {
cb();
});
i.src = src;
i.id = id;
context.appendChild(i);
}
function createFrames(url, callback) {
var frame, num = 2;
var loaded = 0;
if (typeof url === 'function'){
callback = url;
url = '../mock/frames/frame-frame.html';
}
function onLoad() {
loaded++;
if (loaded >= (num)) {
callback();
}
}
frame = document.createElement('iframe');
frame.src = url;
frame.addEventListener('load', onLoad);
fixture.appendChild(frame);
frame = document.createElement('iframe');
frame.src = '../mock/frames/nocode.html';
frame.addEventListener('load', onLoad);
fixture.appendChild(frame);
return frame;
}
var fixture = document.getElementById('fixture');
var isNotCalled;
beforeEach(function () {
isNotCalled = function (err) {
throw err || new Error('Reject should not be called');
};
});
afterEach(function () {
fixture.innerHTML = '';
axe._audit = null;
axe._tree = undefined;
});
it('should work', function (done) {
axe._load({ rules: [{
id: 'html',
selector: 'html',
any: ['html']
}], checks: [{
id: 'html',
evaluate: function () {
return true;
}
}], messages: {}});
var frame = document.createElement('iframe');
frame.src = '../mock/frames/frame-frame.html';
frame.addEventListener('load', function () {
setTimeout(function () {
runRules(document, {}, function (r) {
assert.lengthOf(r[0].passes, 3);
done();
}, isNotCalled);
}, 500);
});
fixture.appendChild(frame);
});
it('should properly order iframes', function (done) {
axe._load({ rules: [{
id: 'iframe',
selector: 'iframe',
any: ['iframe']
}], checks:[{
id: 'iframe',
evaluate: function () {
return true;
}
}], messages: {}});
var frame = document.createElement('iframe');
frame.addEventListener('load', function () {
setTimeout(function () {
runRules(document, {}, function (r) {
var nodes = r[0].passes.map(function (detail) {
return detail.node.selector;
});
assert.deepEqual(nodes, [
['#level0'],
['#level0', '#level1'],
['#level0', '#level1', '#level2a'],
['#level0', '#level1', '#level2b']
]);
done();
}, isNotCalled);
}, 500);
});
frame.id = 'level0';
frame.src = '../mock/frames/nested0.html';
fixture.appendChild(frame);
});
it('should properly calculate context and return results from matching frames', function (done) {
axe._load({
rules: [{
id: 'div#target',
selector: '#target',
any: ['has-target']
}, {
id: 'first-div',
selector: 'div:not(#fixture)',
any: ['first-div']
}],
checks: [{
id: 'has-target',
evaluate: function () {
return true;
}
}, {
id: 'first-div',
evaluate: function (node) {
this.relatedNodes([node]);
return false;
},
after: function (results) {
if (results.length) {
results[0].result = true;
}
return [results[0]];
}
}],
messages: {}
});
iframeReady('../mock/frames/context.html', fixture, 'context-test', function () {
var div = document.createElement('div');
fixture.appendChild(div);
runRules('#fixture', {}, function (results) {
assert.deepEqual(JSON.parse(JSON.stringify(results)), [{
id: 'div#target',
helpUrl: 'https://dequeuniversity.com/rules/axe/' + ver + '/div#target?application=axeAPI',
pageLevel: false,
impact: null,
inapplicable: [],
incomplete: [],
violations: [],
passes: [{
result: 'passed',
impact: null,
node: {
selector: ['#context-test', '#target'],
xpath: ['/iframe[@id=\'context-test\']', '/div[@id=\'target\']'],
source: '<div id="target"></div>'
},
any: [{
id: 'has-target',
data: null,
relatedNodes: []
}],
all: [],
none: []
}],
result: 'passed',
tags: []
}, {
id: 'first-div',
helpUrl: 'https://dequeuniversity.com/rules/axe/' + ver + '/first-div?application=axeAPI',
pageLevel: false,
impact: null,
inapplicable: [],
incomplete: [],
violations: [],
passes: [{
result: 'passed',
impact: null,
node: {
selector: ['#context-test', '#foo'],
xpath: ['/iframe[@id=\'context-test\']', '/div[@id=\'foo\']'],
source: '<div id="foo">\n <div id="bar"></div>\n </div>'
},
any: [{
id: 'first-div',
data: null,
relatedNodes: [{
selector: ['#context-test', '#foo'],
xpath: ['/iframe[@id=\'context-test\']', '/div[@id=\'foo\']'],
source: '<div id="foo">\n <div id="bar"></div>\n </div>'
}]
}],
all: [],
none: []
}],
result: 'passed',
tags: []
}]);
done();
}, isNotCalled);
});
});
it('should reject if the context is invalid', function (done) {
axe._load({
rules: [{
id: 'div#target',
selector: '#target',
any: ['has-target']
}],
messages: {}
});
iframeReady('../mock/frames/context.html', fixture, 'context-test', function () {
runRules('#not-happening', {}, function () {
assert.fail('This selector should not exist.');
}, function (error) {
assert.isOk(error);
assert.equal(error.message, 'No elements found for include in page Context');
done();
});
});
});
it('should accept a jQuery-like object', function (done) {
axe._load({
rules: [{
id: 'test',
selector: '*',
none: ['bob']
}],
checks: [{
id: 'bob',
evaluate: function () {
return true;
}
}]
});
fixture.innerHTML = '<div id="t1"><span></span></div><div id="t2"><em></em></div>';
var $test = {
0: fixture.querySelector('#t1'),
1: fixture.querySelector('#t2'),
length: 2
};
axe.run($test, function (err, results) {
assert.isNull(err);
assert.lengthOf(results.violations, 1);
assert.lengthOf(results.violations[0].nodes, 4);
assert.deepEqual(results.violations[0].nodes[0].target, ['#t1']);
assert.deepEqual(results.violations[0].nodes[1].target, ['#t1 > span']);
assert.deepEqual(results.violations[0].nodes[2].target, ['#t2']);
assert.deepEqual(results.violations[0].nodes[3].target, ['#t2 > em']);
done();
});
});
it('should accept a NodeList', function (done) {
axe._load({
rules: [{
id: 'test',
selector: '*',
none: ['fred']
}],
checks: [{
id: 'fred',
evaluate: function () {
return true;
}
}]
});
fixture.innerHTML = '<div class="foo" id="t1"><span></span></div><div class="foo" id="t2"><em></em></div>';
var test = fixture.querySelectorAll('.foo');
axe.run(test, function (err, results) {
assert.isNull(err);
assert.lengthOf(results.violations, 1);
assert.lengthOf(results.violations[0].nodes, 4);
assert.deepEqual(results.violations[0].nodes[0].target, ['#t1']);
assert.deepEqual(results.violations[0].nodes[1].target, ['#t1 > span']);
assert.deepEqual(results.violations[0].nodes[2].target, ['#t2']);
assert.deepEqual(results.violations[0].nodes[3].target, ['#t2 > em']);
done();
});
});
it('should pull metadata from configuration', function (done) {
axe._load({
rules: [{
id: 'div#target',
selector: '#target',
any: ['has-target']
}, {
id: 'first-div',
selector: 'div#fixture div',
any: ['first-div'],
}],
checks: [{
id: 'has-target',
evaluate: function () {
return false;
}
}, {
id: 'first-div',
evaluate: function (node) {
this.relatedNodes([node]);
return false;
},
after: function (results) {
if (results.length) {
results[0].result = true;
}
return [results[0]];
}
}],
data: {
rules: {
'div#target': {
foo: 'bar',
stuff: 'blah'
},
'first-div': {
bar: 'foo',
stuff: 'no'
}
},
checks: {
'first-div': {
thingy: true,
impact: 'serious',
messages: {
fail: function (checkResult) {
return checkResult.id === 'first-div' ? 'failing is not good' : 'y u wrong rule?';
},
pass: function (checkResult) {
return checkResult.id === 'first-div' ? 'passing is good' : 'y u wrong rule?';
}
}
},
'has-target': {
otherThingy: true,
impact: 'moderate',
messages: {
fail: function (checkResult) {
return checkResult.id === 'has-target' ? 'failing is not good' : 'y u wrong rule?';
},
pass: function (checkResult) {
return checkResult.id === 'has-target' ? 'passing is good' : 'y u wrong rule?';
}
}
}
}
}
});
fixture.innerHTML = '<div id="target">Target!</div><div>ok</div>';
runRules('#fixture', {}, function (results) {
assert.deepEqual(JSON.parse(JSON.stringify(results)), [{
id: 'div#target',
helpUrl: 'https://dequeuniversity.com/rules/axe/' + ver + '/div#target?application=axeAPI',
pageLevel: false,
foo: 'bar',
stuff: 'blah',
impact: 'moderate',
passes: [],
inapplicable: [],
incomplete: [],
violations: [{
result: 'failed',
node: {
selector: ['#target'],
xpath: ['/div[@id=\'target\']'],
source: '<div id="target">Target!</div>'
},
impact: 'moderate',
any: [{
impact: 'moderate',
otherThingy: true,
message: 'failing is not good',
id: 'has-target',
data: null,
relatedNodes: []
}],
all: [],
none: []
}],
result: 'failed',
tags: []
}, {
id: 'first-div',
helpUrl: 'https://dequeuniversity.com/rules/axe/' + ver + '/first-div?application=axeAPI',
pageLevel: false,
bar: 'foo',
stuff: 'no',
impact: null,
inapplicable: [],
incomplete: [],
violations: [],
passes: [{
result: 'passed',
impact: null,
node: {
selector: ['#target'],
xpath: ['/div[@id=\'target\']'],
source: '<div id="target">Target!</div>'
},
any: [{
impact: 'serious',
id: 'first-div',
thingy: true,
message: 'passing is good',
data: null,
relatedNodes: [{
selector: ['#target'],
xpath: ['/div[@id=\'target\']'],
source: '<div id="target">Target!</div>'
}]
}],
all: [],
none: []
}],
result: 'passed',
tags: []
}]);
done();
}, isNotCalled);
});
it('should call the reject argument if an error occurs', function (done) {
axe._load({ rules: [{
id: 'invalidRule'
}], checks: [], messages: {}});
createFrames(function () {
setTimeout(function () {
runRules(document, {}, function () {
assert.ok(false, 'You shall not pass!');
done();
},
function (err) {
assert.instanceOf(err, Error);
done();
});
}, 100);
});
});
it('should resolve to cantTell when a rule fails', function (done) {
axe._load({
rules: [{
id: 'incomplete-1',
selector: '*',
none: ['undeffed']
}, {
id: 'incomplete-2',
selector: '*',
none: ['thrower']
}],
checks: [{
id: 'undeffed',
evaluate: function () {
return undefined;
}
}, {
id: 'thrower',
evaluate: function () {
throw new Error('Check failed to complete');
}
}]
});
fixture.innerHTML = '<div></div>';
axe.run('#fixture', function (err, results) {
assert.isNull(err);
assert.lengthOf(results.incomplete, 2);
assert.equal(results.incomplete[0].id, 'incomplete-1');
assert.equal(results.incomplete[1].id, 'incomplete-2');
assert.include(results.incomplete[1].description,
'An error occured while running this rule');
done();
});
});
it('should resolve to cantTell if an error occurs inside frame rules', function (done) {
axe._load({
rules: [{
id: 'incomplete-1',
selector: '.nogo',
none: ['undeffed']
}, {
id: 'incomplete-2',
selector: '.nogo',
none: ['thrower']
}],
checks: [{
id: 'undeffed',
evaluate: function () {
return false;
}
}, {
id: 'thrower',
evaluate: function () {
return false;
}
}]
});
iframeReady('../mock/frames/rule-error.html', fixture, 'context-test', function () {
axe.run('#fixture', function (err, results) {
assert.isNull(err);
assert.lengthOf(results.incomplete, 2);
assert.equal(results.incomplete[0].id, 'incomplete-1');
assert.equal(results.incomplete[1].id, 'incomplete-2');
assert.include(results.incomplete[1].description,
'An error occured while running this rule');
done();
});
});
});
it('should cascade `no elements found` errors in frames to reject run_rules', function (done) {
axe._load({ rules: [{
id: 'invalidRule'
}], checks: [], messages: {}});
fixture.innerHTML = '<div id="outer"></div>';
var outer = document.getElementById('outer');
iframeReady('../mock/frames/context.html', outer, 'target', function() {
runRules([['#target', '#elementNotFound']], {}, function resolve() {
assert.ok(false, 'frame should have thrown an error');
}, function reject(err) {
assert.instanceOf(err, Error);
assert.include(err.message, 'No elements found for include in frame Context');
done();
});
});
});
it('should not call reject when the resolve throws', function (done) {
var rejectCalled = false;
axe._load({ rules: [{
id: 'html',
selector: 'html',
any: ['html']
}], checks: [{
id: 'html',
evaluate: function () {
return true;
}
}], messages: {}});
function resolve() {
setTimeout(function () {
assert.isFalse(rejectCalled);
axe.log = log;
done();
}, 20);
throw new Error('err');
}
function reject() {
rejectCalled = true;
}
var log = axe.log;
axe.log = function (e) {
assert.equal(e.message, 'err');
axe.log = log;
};
runRules(document, {},resolve, reject);
});
it('should ignore iframes if `iframes` === false', function (done) {
axe._load({ rules: [{
id: 'html',
selector: 'html',
any: ['html']
}], checks: [{
id: 'html',
evaluate: function () {
return true;
}
}], messages: {}});
var frame = document.createElement('iframe');
frame.src = '../mock/frames/frame-frame.html';
frame.addEventListener('load', function () {
setTimeout(function () {
runRules(document, { iframes: false }, function (r) {
assert.lengthOf(r[0].passes, 1);
assert.equal(r[0].passes[0].node.element.ownerDocument, document,
'Result should not be in an iframe');
done();
}, isNotCalled);
}, 500);
});
fixture.appendChild(frame);
});
it('should not fail if `include` / `exclude` is overwritten', function (done) {
function invalid () {
throw new Error('nope!');
}
Array.prototype.include = invalid;
Array.prototype.exclude = invalid;
axe._load({ rules: [{
id: 'html',
selector: 'html',
any: ['html']
}], checks: [{
id: 'html',
evaluate: function () {
return true;
}
}], messages: {}});
runRules([document], {}, function (r) {
assert.lengthOf(r[0].passes, 1);
delete Array.prototype.include;
delete Array.prototype.exclude;
done();
}, isNotCalled);
});
it('should return a cleanup method', function (done) {
axe._load({ rules: [{
id: 'html',
selector: 'html',
any: ['html']
}], checks: [{
id: 'html',
evaluate: function () {
return true;
}
}], messages: {}});
runRules(document, {}, function resolve(out, cleanup) {
assert.isDefined(axe._tree);
assert.isDefined(axe._selectorData);
cleanup();
assert.isUndefined(axe._tree);
assert.isUndefined(axe._selectorData);
done();
}, isNotCalled);
});
it('should clear up axe._tree / axe._selectorData after an error', function (done) {
axe._load({ rules: [{
id: 'invalidRule'
}], checks: [], messages: {}});
createFrames(function () {
setTimeout(function () {
runRules(document, {}, isNotCalled, function () {
assert.isUndefined(axe._tree);
assert.isUndefined(axe._selectorData);
done();
});
}, 100);
});
});
});