pa11y
Version:
Pa11y is your automated accessibility testing pal
244 lines (220 loc) • 6.67 kB
JavaScript
/* eslint strict: ["error", "function"] */
((exporter => {
'use strict';
const pa11yRunner = {
getElementContext,
getElementSelector,
run,
runners: {},
version: null
};
// Create the global Pa11y variable
/* eslint-disable-next-line no-underscore-dangle */
exporter.__pa11y = pa11yRunner;
/**
* A map of issue names to type codes.
* @private
*/
const issueCodeMap = {
unknown: 0,
error: 1,
warning: 2,
notice: 3
};
/**
* @import { Pa11yConfiguration } from './pa11y'
*/
/**
* Run Pa11y on the current page, using any other runners defined.
* @public
* @param {Pa11yConfiguration} options Options to use when running tests.
* @returns {Promise} Returns a promise which resolves with test results.
*/
async function run(options) {
const {
pa11yVersion,
runners: runnerNames,
wait: waitTime
} = options;
await wait(waitTime);
pa11yRunner.version = pa11yVersion;
const issuesPerRunner = await Promise.all(
runnerNames.map(runnerGo)
);
return {
documentTitle: window.document.title || '',
pageUrl: window.location.href || '',
issues: processIssues(issuesPerRunner.flat())
};
async function runnerGo(name) {
const issues = await pa11yRunner.runners[name](options, pa11yRunner);
return issues.map(issue => ({
...issue,
runner: name
}));
}
/**
* Process issues from a runner.
* @param {Array<Pa11yIssue>} issues An array of issues to process.
* @returns {Array<Pa11yIssue>} Returns an array of processed issues.
*/
function processIssues(issues) {
if (options.rootElement) {
issues = issues.filter(issue => isElementInTestArea(issue.element));
}
if (options.hideElements) {
issues = issues.filter(issue => isElementOutsideHiddenArea(issue.element));
}
return issues.map(processIssue).filter(isIssueNotIgnored);
}
/**
* Process a runner issue
* @private
* @param {Pa11yIssue} issue An unrefined Pa11y runner issue
* @returns {Pa11yIssue} The completed issue
*/
function processIssue({code, type, message, element, runner, runnerExtras = {}}) {
return {
code,
type,
typeCode: issueCodeMap[type] || 0,
message,
context: (element ? getElementContext(element) : ''),
selector: (element ? getElementSelector(element) : ''),
runner,
runnerExtras
};
}
/**
* Confirms the issue isn't being ignored.
* @param {Pa11yIssue} issue Some issue.
* @returns {boolean} Whether the issue should be included.
*/
function isIssueNotIgnored(issue) {
if (options.ignore.indexOf(issue.code.toLowerCase()) !== -1) {
return false;
}
if (options.ignore.indexOf(issue.type) !== -1) {
return false;
}
return true;
}
/**
* Check whether an element is in the test area specified by rootElement.
* @private
* @param {HTMLElement} element Some element.
* @returns {boolean} Whether the element is in the test area.
*/
function isElementInTestArea(element) {
const rootElement = window.document.querySelector(options.rootElement);
return (rootElement ? rootElement.contains(element) : true);
}
/**
* Check whether an element is outside of all hidden selectors.
* @private
* @param {HTMLElement} element Some element.
* @returns {boolean} Whether the element is outside of a hidden area.
*/
function isElementOutsideHiddenArea(element) {
const hiddenElements = [...window.document.querySelectorAll(options.hideElements)];
return !hiddenElements.some(hiddenElement => {
return hiddenElement.contains(element);
});
}
}
/**
* Wait for some time.
* @param {number} milliseconds Number of milliseconds to wait.
* @returns {Promise<void>} A promise to continue after some time passes.
*/
function wait(milliseconds) {
return new Promise(resolve => {
setTimeout(resolve, milliseconds);
});
}
/**
* Get a short version of an element's outer HTML.
* @param {HTMLElement} element Some element.
* @returns {string} Shortened HTML as string.
*/
function getElementContext(element) {
let outerHTML = null;
let innerHTML = null;
if (!element.outerHTML) {
return outerHTML;
}
({outerHTML} = element);
if (element.innerHTML.length > 31) {
innerHTML = `${element.innerHTML.substr(0, 31)}...`;
outerHTML = outerHTML.replace(element.innerHTML, innerHTML);
}
if (outerHTML.length > 251) {
outerHTML = `${outerHTML.substr(0, 250)}...`;
}
return outerHTML;
}
/**
* Get a CSS selector for an element.
* @param {HTMLElement} element - An element to get a selector for.
* @param {Array} [selectorParts=[]] - Internal parameter used for recursion.
* @returns {String} Returns the CSS selector as a string.
*/
function getElementSelector(element, selectorParts = []) {
if (nodeIsElement(element)) {
const identifier = buildElementIdentifier(element);
selectorParts.unshift(identifier);
if (!element.id && element.parentNode) {
return getElementSelector(element.parentNode, selectorParts);
}
}
return selectorParts.join(' > ');
}
/**
* Build a unique CSS element identifier.
* @param {HTMLElement} element - An element to get a CSS element identifier for.
* @returns {string} CSS element identifier
*/
function buildElementIdentifier(element) {
if (element.id) {
return `#${element.id}`;
}
let identifier = element.tagName.toLowerCase();
if (!element.parentNode) {
return identifier;
}
const siblings = getSiblings(element);
const childIndex = siblings.indexOf(element);
if (!isOnlySiblingOfType(element, siblings) && childIndex !== -1) {
identifier += `:nth-child(${childIndex + 1})`;
}
return identifier;
}
/**
* Get element siblings.
* @param {HTMLElement} element Some element.
* @returns {Array<HTMLElement>} Array of siblings.
*/
function getSiblings(element) {
return [...element.parentNode.childNodes].filter(nodeIsElement);
}
/**
* Check whether an element is the only sibling of its type.
* @param {HTMLElement} element Some element.
* @param {Array<HTMLElement>} siblings Siblings of this element.
* @returns {boolean} Whether the element is the only sibling of its type.
*/
function isOnlySiblingOfType(element, siblings) {
const siblingsOfType = siblings.filter(sibling => {
return (sibling.tagName === element.tagName);
});
return (siblingsOfType.length <= 1);
}
/**
* Check whether a node is an element.
* @param {Node} node Some DOM node.
* @returns {boolean} Returns whether the element is an HTMLElement.
*/
function nodeIsElement(node) {
return (node.nodeType === window.Node.ELEMENT_NODE);
}
})(this));