UNPKG

@axe-core/react

Version:

Dynamic accessibility analysis for React using axe-core

319 lines (315 loc) 8.81 kB
// index.ts import axeCore from "axe-core"; import * as rIC from "requestidlecallback"; // after.ts var restoreFunctions = []; function after(host, name, cb) { const originalFn = host[name]; let restoreFn; if (originalFn) { host[name] = function(...args) { originalFn.apply(this, args); cb(host); }; restoreFn = function() { host[name] = originalFn; }; } else { host[name] = function() { cb(host); }; restoreFn = function() { delete host[name]; }; } restoreFunctions.push(restoreFn); } after.restorePatchedMethods = function() { restoreFunctions.forEach((restoreFn) => restoreFn()); restoreFunctions = []; }; // cache.ts var _cache = {}; var cache = { set(key, value) { _cache[key] = value; }, get(key) { return _cache[key]; }, clear() { Object.keys(_cache).forEach((key) => { delete _cache[key]; }); } }; var cache_default = cache; // index.ts var requestIdleCallback = rIC.request; var cancelIdleCallback = rIC.cancel; var React; var ReactDOM; var logger; var lightTheme = { serious: "#d93251", minor: "#d24700", text: "black" }; var darkTheme = { serious: "#ffb3b3", minor: "#ffd500", text: "white" }; var theme = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? darkTheme : lightTheme; var boldCourier = "font-weight:bold;font-family:Courier;"; var critical = `color:${theme.serious};font-weight:bold;`; var serious = `color:${theme.serious};font-weight:normal;`; var moderate = `color:${theme.minor};font-weight:bold;`; var minor = `color:${theme.minor};font-weight:normal;`; var defaultReset = `font-color:${theme.text};font-weight:normal;`; var idleId; var timeout; var context; var conf; var _createElement; var components = {}; var nodes = [document.documentElement]; function debounce(func, wait, immediate) { let _timeout; return function(...args) { const later = () => { _timeout = null; if (!immediate) func.apply(this, args); }; const callNow = immediate && !_timeout; clearTimeout(_timeout); _timeout = setTimeout(later, wait); if (callNow) func.apply(this, args); }; } function getPath(node) { const path = [node]; while (node && node.nodeName.toLowerCase() !== "html") { path.push(node.parentNode); node = node.parentNode; } if (!node || !node.parentNode) { return null; } return path.reverse(); } function getCommonParent(nodes2) { let path; let nextPath; if (nodes2.length === 1) { return nodes2.pop(); } while (!path && nodes2.length) { path = getPath(nodes2.pop()); } while (nodes2.length) { nextPath = getPath(nodes2.pop()); if (nextPath) { path = path.filter((node, index) => { return nextPath.length > index && nextPath[index] === node; }); } } return path ? path[path.length - 1] : document; } function logElement(node, logFn) { const el = document.querySelector(node.target.toString()); if (!el) { logFn("Selector: %c%s", boldCourier, node.target.toString()); } else { logFn("Element: %o", el); } } function logHtml(node) { console.log("HTML: %c%s", boldCourier, node.html); } function logFailureMessage(node, key) { const message = axeCore._audit.data.failureSummaries[key].failureMessage( node[key].map((check) => check.message || "") ); console.error(message); } function failureSummary(node, key) { if (node[key].length > 0) { logElement(node, console.groupCollapsed); logHtml(node); logFailureMessage(node, key); let relatedNodes = []; node[key].forEach((check) => { relatedNodes = relatedNodes.concat(check.relatedNodes); }); if (relatedNodes.length > 0) { console.groupCollapsed("Related nodes"); relatedNodes.forEach((relatedNode) => { logElement(relatedNode, console.log); logHtml(relatedNode); }); console.groupEnd(); } console.groupEnd(); } } function checkAndReport(node, timeout2) { const disableDeduplicate = conf["disableDeduplicate"]; if (idleId) { cancelIdleCallback(idleId); idleId = void 0; } return new Promise((resolve, reject) => { nodes.push(node); idleId = requestIdleCallback( () => { let n = context; if (n === void 0) { n = getCommonParent(nodes.filter((node2) => node2.isConnected)); if (n.nodeName.toLowerCase() === "html") { n = document; } } axeCore.configure({ allowedOrigins: ["<unsafe_all_origins>"] }); axeCore.run( n, { reporter: "v2" }, function(error, results) { if (error) { return reject(error); } results.violations = results.violations.filter((result) => { result.nodes = result.nodes.filter((node2) => { const key = node2.target.toString() + result.id; const retVal = !cache_default.get(key); cache_default.set(key, key); return disableDeduplicate || retVal; }); return !!result.nodes.length; }); if (results.violations.length) { logger(results); } resolve(); } ); }, { timeout: timeout2 } ); }); } function checkNode(component) { let node; try { node = ReactDOM.findDOMNode(component); } catch (e) { console.group("%caxe error: could not check node", critical); console.group("%cComponent", serious); console.error(component); console.groupEnd(); console.group("%cError", serious); console.error(e); console.groupEnd(); console.groupEnd(); } if (node) { checkAndReport(node, timeout); } } function componentAfterRender(component) { const debounceCheckNode = debounce(checkNode, timeout, true); after(component, "componentDidMount", debounceCheckNode); after(component, "componentDidUpdate", debounceCheckNode); } function addComponent(component) { const reactInstance = component._reactInternalInstance || {}; const reactInstanceDebugID = reactInstance._debugID; const reactFiberInstance = component._reactInternalFiber || {}; const reactFiberInstanceDebugID = reactFiberInstance._debugID; const reactInternals = component._reactInternals || {}; const reactInternalsDebugID = reactInternals._debugID; if (reactInstanceDebugID && !components[reactInstanceDebugID]) { components[reactInstanceDebugID] = component; componentAfterRender(component); } else if (reactFiberInstanceDebugID && !components[reactFiberInstanceDebugID]) { components[reactFiberInstanceDebugID] = component; componentAfterRender(component); } else if (reactInternalsDebugID && !components[reactInternalsDebugID]) { components[reactInternalsDebugID] = component; componentAfterRender(component); } } function logToConsole(results) { console.group("%cNew axe issues", serious); results.violations.forEach((result) => { let fmt; switch (result.impact) { case "critical": fmt = critical; break; case "serious": fmt = serious; break; case "moderate": fmt = moderate; break; case "minor": fmt = minor; break; default: fmt = minor; break; } console.groupCollapsed( "%c%s: %c%s %s", fmt, result.impact, defaultReset, result.help, result.helpUrl ); result.nodes.forEach((node) => { failureSummary(node, "any"); failureSummary(node, "none"); }); console.groupEnd(); }); console.groupEnd(); } function reactAxe(_React, _ReactDOM, _timeout, _conf = {}, _context, _logger) { React = _React; ReactDOM = _ReactDOM; timeout = _timeout; context = _context; conf = _conf; logger = _logger || logToConsole; const runOnly = conf["runOnly"]; if (runOnly) { conf["rules"] = axeCore.getRules(runOnly).map((rule) => ({ ...rule, id: rule.ruleId, enabled: true })); conf["disableOtherRules"] = true; } if (Object.keys(conf).length > 0) { axeCore.configure(conf); } axeCore.configure({ allowedOrigins: ["<unsafe_all_origins>"] }); if (!_createElement) { _createElement = React.createElement; React.createElement = function(...args) { const reactEl = _createElement.apply(this, args); if (reactEl._owner && reactEl._owner._instance) { addComponent(reactEl._owner._instance); } else if (reactEl._owner && reactEl._owner.stateNode) { addComponent(reactEl._owner.stateNode); } return reactEl; }; } return checkAndReport(document.body, timeout); } export { reactAxe as default, logToConsole };