blob: ad3d016452cac37937d18ff41407914dbd71f7d1 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/** @fileoverview Accessibility Test API */
GEN_INCLUDE([
'accessibility_audit_rules.js',
'//third_party/axe-core/axe.js',
]);
/**
* Accessibility Test
* @namespace
*/
var AccessibilityTest = AccessibilityTest || {};
/**
* @typedef {{
* runOnly: {
* type: string,
* values: Array<string>
* }
* }}
* @see https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#options-parameter
*/
AccessibilityTest.AxeOptions;
/**
* The violation filter object maps individual audit rule IDs to functions that
* return true for elements to filter from that rule's violations.
* @typedef {Object<string, function(!axe.NodeResult): boolean>}
*/
AccessibilityTest.ViolationFilter;
/**
* @typedef {{
* name: string,
* axeOptions: ?AccessibilityTest.AxeOptions,
* setup: ?function,
* tests: Object<string, function(): ?Promise>,
* violationFilter: ?AccessibilityTest.ViolationFilter
* }}
*/
AccessibilityTest.Definition;
/**
* Run aXe-core accessibility audit, print console-friendly representation
* of violations to console, and fail the test.
* @param {!AccessibilityTest.Definition} testDef Object configuring the audit.
* @return {Promise} A promise that will be resolved with the accessibility
* audit is complete.
*/
AccessibilityTest.runAudit_ = function(testDef) {
// Ignore iron-iconset-svg elements that have duplicate ids and result in
// false postives from the audit.
let context = {exclude: ['iron-iconset-svg']};
let options = testDef.axeOptions || {};
// Element references needed for filtering audit results.
options.elementRef = true;
return new Promise((resolve, reject) => {
axe.run(context, options, (err, results) => {
if (err) {
reject(err);
}
let filteredViolations = AccessibilityTest.filterViolations_(
results.violations, testDef.violationFilter || {});
let violationCount = filteredViolations.length;
if (violationCount) {
AccessibilityTest.print_(filteredViolations);
reject('Found ' + violationCount + ' accessibility violations.');
} else {
resolve();
}
});
});
};
/*
* Get list of filtered audit violations.
* @param {!Array<axe.Result>} violations List of accessibility violations.
* @param {!AccessibilityTest.ViolationFilter} filter Object specifying set of
* violations to filter from the results.
* @return {!Array<axe.Result>} List of filtered violations.
*/
AccessibilityTest.filterViolations_ = function(violations, filter) {
if (Object.keys(filter).length == 0) {
return violations;
}
let filteredViolations = [];
// Check for and remove any nodes specified by filter.
for (let violation of violations) {
if (violation.id in filter) {
let exclusionRule = filter[violation.id];
violation.nodes = violation.nodes.filter((node) => !exclusionRule(node));
}
if (violation.nodes.length > 0) {
filteredViolations.push(violation);
}
}
return filteredViolations;
};
/**
* Define a GTest test for each audit rule.
* @param {string} testFixture Name of test fixture associated with the test.
* @param {AccessibilityTestDefinition} testDef Object configuring the test.
* @constructor
*/
AccessibilityTest.define = function(testFixture, testDef) {
// Disable in debug mode because of timeouts.
GEN('#if defined(NDEBUG)');
let axeOptions = testDef.axeOptions || {};
testDef.setup = testDef.setup || (() => {});
// Define a test for each audit rule separately.
let rules = axeOptions.runOnly ? axeOptions.runOnly.values :
AccessibilityTest.ruleIds;
rules.forEach((ruleId) => {
// Skip rules disabled in axeOptions.
if (axeOptions.rules && ruleId in axeOptions.rules &&
!axeOptions.rules[ruleId].enabled) {
return;
}
let newTestDef = Object.assign({}, testDef);
newTestDef.name += '_' + ruleId;
// Replace hyphens, which break the build.
newTestDef.name = newTestDef.name.replace(new RegExp('-', 'g'), '_');
newTestDef.axeOptions = Object.assign({}, axeOptions);
newTestDef.axeOptions.runOnly = {type: 'rule', values: [ruleId]};
TEST_F(testFixture, newTestDef.name, () => {
// Define the mocha tests
suite(newTestDef.name, () => {
setup(newTestDef.setup.bind(newTestDef));
for (let testMember in newTestDef.tests) {
test(
testMember,
AccessibilityTest.getMochaTest_(testMember, newTestDef));
}
});
mocha.grep(newTestDef.name).run();
});
});
GEN('#endif // defined(NDEBUG)');
};
/**
*
* Return a function that runs the accessibility audit after executing
* the function corresponding to the |testDef.tests.testMember|.
* @param {string} testMember The name of the mocha test
* @param {AccessibilityTestDefinition} testDef Object configuring the test
* suite to which this test belongs.
*/
AccessibilityTest.getMochaTest_ = function(testMember, testDef) {
return () => {
// Run commands specified by the test definition followed by the
// accessibility audit.
let promise = testDef.tests[testMember].call(testDef);
if (promise) {
return promise.then(() => AccessibilityTest.runAudit_(testDef));
} else {
return AccessibilityTest.runAudit_(testDef);
}
};
};
/**
* Remove circular references in |violations| and print violations to the
* console.
* @param {!Array<axe.Result>} List of violations to display
*/
AccessibilityTest.print_ = function(violations) {
// Elements have circular references and must be removed before printing.
for (let violation of violations) {
for (let node of violation.nodes) {
delete node['element'];
['all', 'any', 'none'].forEach((attribute) => {
for (let checkResult of node[attribute]) {
for (let relatedNode of checkResult.relatedNodes) {
delete relatedNode['element'];
}
}
});
}
}
console.log(JSON.stringify(violations, null, 4));
};