accessibility-checker
Version:
An automated testing tools for accessibility testing using Puppeteer, Selenium, or Zombie
614 lines (609 loc) • 25.3 kB
JavaScript
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname, join, resolve as pathResolve } from "path";
import { ACBrowserManager } from "./ACBrowserManager.js";
import { ACEngineManager } from "./ACEngineManager.js";
import { ACConfigManager } from "./common/config/ACConfigManager.js";
import { ReporterManager } from "./common/report/ReporterManager.js";
import { BaselineManager } from "./common/report/BaselineManager.js";
let loggerCreate = function (type) {
return logger;
};
let logger = {
debug: (...output) => { Config && Config.DEBUG && console.debug(...output); },
info: (...output) => { Config && Config.DEBUG && console.info(...output); },
error: (...output) => { Config && Config.DEBUG && console.error(...output); },
warn: (...output) => { Config && Config.DEBUG && console.warn(...output); },
create: loggerCreate
};
let Config;
let checkPolicy = false;
class MyFS {
writeFileSync(filePath, data) {
let outFile = this.prepFileSync(filePath);
writeFileSync(outFile, data);
}
prepFileSync(filePath) {
let outDir = pathResolve(Config.outputFolder);
let outFile = join(outDir, filePath);
if (!existsSync(dirname(outFile))) {
mkdirSync(dirname(outFile), { recursive: true });
}
return outFile;
}
log(...output) { Config && Config.DEBUG && console.debug(...output); }
info(...output) { Config && Config.DEBUG && console.info(...output); }
error(...output) { Config && Config.DEBUG && console.error(...output); }
loadBaseline(label) {
let baselineFile = join(join(process.cwd(), Config.baselineFolder), label + ".json");
if (!existsSync(baselineFile))
return null;
if (typeof require !== "undefined") {
return require(baselineFile);
}
else {
return JSON.parse(readFileSync(baselineFile).toString());
}
}
getChecker() {
return ACEngineManager.getChecker();
}
}
async function initialize() {
if (Config)
return;
Config = await ACConfigManager.getConfigUnsupported();
await ACEngineManager.loadEngineLocal();
let absAPI = new MyFS();
let refactorMap = {};
let rules = ACEngineManager.getRulesSync();
for (const rule of rules) {
if (rule.refactor) {
for (const key in rule.refactor) {
refactorMap[key] = rule;
}
}
}
ReporterManager.initialize(Config, absAPI, await ACEngineManager.getRulesets());
BaselineManager.initialize(Config, absAPI, refactorMap);
}
(async () => {
try {
// If cucumber is the platform...
let module = (await import("cucumber"));
let { AfterAll } = require('cucumber');
if (module.default.AfterAll) {
module.default.AfterAll(function (done) {
// const rulePack = `${Config.rulePack}`;
initialize()
.then(() => ReporterManager.generateSummaries())
.then(() => ACBrowserManager.close())
.then(done);
});
}
}
catch (e) {
if (typeof (after) !== "undefined") {
after(function (done) {
if (Config) {
if (this.timeout) {
this.timeout(300000);
}
// const rulePack = `${Config.rulePack}/ace`;
initialize()
.then(() => ReporterManager.generateSummaries())
.then(() => ACBrowserManager.close())
.then(done);
}
else {
done();
}
});
}
else {
process.on('beforeExit', async function () {
if (Config) {
initialize()
.then(() => ReporterManager.generateSummaries());
ACBrowserManager.close();
}
});
}
}
})();
function areValidPolicy(valPolicies, curPol) {
let isValPol = false;
let errorPolicy = "";
for (let i = 0; i < curPol.length; ++i) {
if (valPolicies.indexOf(curPol[i]) === -1) {
errorPolicy += "" + curPol[i] + ",";
}
else {
isValPol = true;
}
}
if (errorPolicy.length > 0) {
errorPolicy = errorPolicy.substr(0, errorPolicy.length - 1);
console.log(`[WARN] InvalidPolicies: Invalid policies "${errorPolicy}". Valid policy ids are: ${valPolicies}`);
}
if (!isValPol) {
const errStr = `[ERROR] ValidPoliciesMissing: No valid policy has been provided. Valid policy ids for the specified archive are: ${valPolicies}`;
console.error(errStr);
throw new Error(errStr);
}
}
export async function getComplianceHelper(content, label) {
await initialize();
Config.DEBUG && console.log("START 'aChecker.getCompliance' function");
if (!content) {
console.error("aChecker: Unable to get compliance of null or undefined object");
//return null;
throw new Error("aChecker: Unable to get compliance of null or undefined object");
}
// Variable Decleration
let URL;
// Since we need to handle multiple variation of possible ways to scan items, we need to handle
// each one differently as each one requires specific actions/setup.
// Handle the following:
// Single node (HTMLElement)
// Multiple node (Array of HTMLElements)
// Local file (String)
// URL (String)
// document
async function getParsed(content) {
if (!content)
return null;
// Handle local file and URL's
if (typeof content === "string") {
let isURLRegex = /^(ftp|http|https):\/\//;
if (isURLRegex.test(content)) {
URL = content;
}
// Since this is a string, we consider this as either URL or local file
// so build an iframe based on this and get the frame doc and then scan this.
return ACBrowserManager.buildIframeAndGetDoc(content);
}
else if (ACEngineManager.isSelenium(content) || ACEngineManager.isPuppeteer(content) || ACEngineManager.isPlaywright(content) || ACEngineManager.isWebDriverIO(content)) {
}
// Handle Array of nodes
else if (content instanceof Array) {
// TODO: Supporting Array of nodes, possible future enhancenment
}
// Handle single node (HTMLElement)
else if (content.nodeType === 1) {
// In the case this is a node, there is nothing special that needs to be done at this time,
// the engine will be able to handle this. Adding this block here as we may need to add some filtering
// of rules or rule sets for this case depending on if a special ruleset needs to be created or not.
content = content;
}
// handle scanning document
else if (content.nodeType === 9) {
// In the case this is a document element, simply send the document object to the engine for now
// we will need to do some filtering to remove any karma related aspects, which requires to do a
// document clone, and then string the karma scripts that are added and then send this document
// to the engine.
// TODO: Investigate best approach to perform filtering
content = content;
}
return content;
}
let parsed = await getParsed(content);
if (!parsed) {
console.error("Invalid content: " + content);
throw new Error("Invalid content: " + content);
}
await ACEngineManager.loadEngine(parsed);
// Get the Data when the scan is started
// Start time will be in milliseconds elapsed since 1 January 1970 00:00:00 UTC up until now.
let policies = Config.policies;
let curPol = null;
if (policies) {
curPol = JSON.parse(JSON.stringify(policies));
}
if (ACEngineManager.isSelenium(parsed)) {
Config.DEBUG && console.log("getComplianceHelper:Selenium");
return await getComplianceHelperSelenium(label, parsed, curPol);
}
else if (ACEngineManager.isPuppeteer(parsed)) {
Config.DEBUG && console.log("ACHelper.ts:getComplianceHelper:Puppeteer");
return await getComplianceHelperPuppeteer(label, parsed, curPol);
}
else if (ACEngineManager.isPlaywright(parsed)) {
Config.DEBUG && console.log("ACHelper.ts:getComplianceHelper:Playwright");
return await getComplianceHelperPuppeteer(label, parsed, curPol);
}
else if (ACEngineManager.isWebDriverIO(parsed)) {
Config.DEBUG && console.log("ACHelper.ts:getComplianceHelper:Playwright");
return await getComplianceHelperWebDriverIO(label, parsed, curPol);
}
else {
Config.DEBUG && console.log("ACHelper.ts:getComplianceHelper:Local");
return await getComplianceHelperLocal(label, parsed, curPol);
}
}
async function getComplianceHelperSelenium(label, parsed, curPol) {
try {
let startScan = Date.now();
// NOTE: Engine should already be loaded
let browser = parsed;
// Selenium
let scriptStr = `let cb = arguments[arguments.length - 1];
try {
const valueToLevel = (reportValue) => {
let reportLevel;
if (reportValue[1] === "PASS") {
reportLevel = "pass";
}
else if ((reportValue[0] === "VIOLATION" || reportValue[0] === "RECOMMENDATION") && reportValue[1] === "MANUAL") {
reportLevel = "manual";
}
else if (reportValue[0] === "VIOLATION") {
if (reportValue[1] === "FAIL") {
reportLevel = "violation";
}
else if (reportValue[1] === "POTENTIAL") {
reportLevel = "potentialviolation";
}
}
else if (reportValue[0] === "RECOMMENDATION") {
if (reportValue[1] === "FAIL") {
reportLevel = "recommendation";
}
else if (reportValue[1] === "POTENTIAL") {
reportLevel = "potentialrecommendation";
}
}
return reportLevel;
}
const getCounts = (engineReport) => {
let counts = {
violation: 0,
potentialviolation: 0,
recommendation: 0,
potentialrecommendation: 0,
manual: 0,
pass: 0
}
for (const issue of engineReport.results) {
++counts[issue.level];
}
return counts;
}
let policies = ${JSON.stringify(Config.policies)};
let checker = new window.ace_ibma.Checker();
let customRulesets = ${JSON.stringify(ACEngineManager.customRulesets)};
customRulesets.forEach((rs) => checker.addRuleset(rs));
setTimeout(function() {
checker.check(document, policies).then(function(report) {
for (const result of report.results) {
delete result.node;
result.level = valueToLevel(result.value)
}
report.summary ||= {};
report.summary.counts ||= getCounts(report);
let reportLevels = ${JSON.stringify((Config.reportLevels || []).concat(Config.failLevels || []).map(lvl => lvl.toString()))};
// Filter out pass results unless they asked for them in reports
// We don't want to mess with baseline functions, but pass results can break the response object
report.results = report.results.filter(result => reportLevels.includes(result.level) || result.level !== "pass");
cb(report);
})
},0)
} catch (e) {
cb(e);
}`;
let manage = browser.manage();
if (manage.timeouts) {
manage.timeouts().setScriptTimeout(60000);
}
else if (manage.setTimeouts) {
manage.setTimeouts({
"script": 60000
});
}
let report = await browser.executeAsyncScript(scriptStr);
const getPolicies = "return new window.ace_ibma.Checker().rulesetIds;";
if (curPol != null && !checkPolicy) {
checkPolicy = true;
const valPolicies = ACEngineManager.customRulesets.map(rs => rs.id).concat(await browser.executeScript(getPolicies));
areValidPolicy(valPolicies, curPol);
}
// If there is something to report...
let finalReport;
if (report.results) {
// Add URL to the result object
const url = await browser.getCurrentUrl();
const title = await browser.getTitle();
let origReport = JSON.parse(JSON.stringify(report));
if (Config.captureScreenshots && browser.takeScreenshot) {
const image = await browser.takeScreenshot();
origReport.screenshot = image;
}
finalReport = ReporterManager.addEngineReport("Selenium", startScan, url, title, label, origReport);
}
return {
"report": finalReport,
"webdriver": parsed
};
}
catch (err) {
console.error(err);
return Promise.reject(err);
}
;
}
async function getComplianceHelperWebDriverIO(label, parsed, curPol) {
try {
const startScan = Date.now();
// NOTE: Engine should already be loaded
const page = parsed;
let report = await page.executeAsync(({ policies, customRulesets, reportLevels }, done) => {
const valueToLevel = (reportValue) => {
let reportLevel;
if (reportValue[1] === "PASS") {
reportLevel = "pass";
}
else if ((reportValue[0] === "VIOLATION" || reportValue[0] === "RECOMMENDATION") && reportValue[1] === "MANUAL") {
reportLevel = "manual";
}
else if (reportValue[0] === "VIOLATION") {
if (reportValue[1] === "FAIL") {
reportLevel = "violation";
}
else if (reportValue[1] === "POTENTIAL") {
reportLevel = "potentialviolation";
}
}
else if (reportValue[0] === "RECOMMENDATION") {
if (reportValue[1] === "FAIL") {
reportLevel = "recommendation";
}
else if (reportValue[1] === "POTENTIAL") {
reportLevel = "potentialrecommendation";
}
}
return reportLevel;
};
const getCounts = (engineReport) => {
let counts = {
violation: 0,
potentialviolation: 0,
recommendation: 0,
potentialrecommendation: 0,
manual: 0,
pass: 0
};
for (const issue of engineReport.results) {
++counts[issue.level];
}
return counts;
};
let checker = new window.ace_ibma.Checker();
customRulesets.forEach((rs) => checker.addRuleset(rs));
return new Promise((resolve, reject) => {
setTimeout(function () {
checker.check(document, policies).then(function (report) {
for (const result of report.results) {
delete result.node;
result.level = valueToLevel(result.value);
}
report.summary ||= {};
report.summary.counts ||= getCounts(report);
// Filter out pass results unless they asked for them in reports
// We don't want to mess with baseline functions, but pass results can break the response object
report.results = report.results.filter(result => reportLevels.includes(result.level) || result.level !== "pass");
resolve(report);
done(report);
});
}, 0);
});
}, { policies: Config.policies, customRulesets: ACEngineManager.customRulesets, reportLevels: (Config.reportLevels || []).concat(Config.failLevels || []).map(lvl => lvl.toString()) });
if (curPol != null && !checkPolicy) {
const valPolicies = ACEngineManager.customRulesets.map(rs => rs.id).concat(await page.execute(() => (new window.ace_ibma.Checker().rulesetIds)));
checkPolicy = true;
areValidPolicy(valPolicies, curPol);
}
let finalReport;
// If there is something to report...
if (report.results) {
let url = await page.execute(() => document.location.href);
let title = await page.execute(() => document.location.title);
let origReport = JSON.parse(JSON.stringify(report));
if (Config.captureScreenshots) {
let image = await page.screenshot({
fullPage: true,
encoding: "base64"
});
origReport.screenshot = image;
}
finalReport = ReporterManager.addEngineReport("Puppeteer", startScan, url, title, label, origReport);
}
page.aceBusy = false;
return {
"report": finalReport,
"puppeteer": parsed
};
}
catch (err) {
console.error(err);
return Promise.reject(err);
}
;
}
async function getComplianceHelperPuppeteer(label, parsed, curPol) {
try {
const startScan = Date.now();
// NOTE: Engine should already be loaded
const page = parsed;
let report = await page.evaluate(({ policies, customRulesets, reportLevels }) => {
const valueToLevel = (reportValue) => {
let reportLevel;
if (reportValue[1] === "PASS") {
reportLevel = "pass";
}
else if ((reportValue[0] === "VIOLATION" || reportValue[0] === "RECOMMENDATION") && reportValue[1] === "MANUAL") {
reportLevel = "manual";
}
else if (reportValue[0] === "VIOLATION") {
if (reportValue[1] === "FAIL") {
reportLevel = "violation";
}
else if (reportValue[1] === "POTENTIAL") {
reportLevel = "potentialviolation";
}
}
else if (reportValue[0] === "RECOMMENDATION") {
if (reportValue[1] === "FAIL") {
reportLevel = "recommendation";
}
else if (reportValue[1] === "POTENTIAL") {
reportLevel = "potentialrecommendation";
}
}
return reportLevel;
};
const getCounts = (engineReport) => {
let counts = {
violation: 0,
potentialviolation: 0,
recommendation: 0,
potentialrecommendation: 0,
manual: 0,
pass: 0
};
for (const issue of engineReport.results) {
++counts[issue.level];
}
return counts;
};
let checker = new window.ace_ibma.Checker();
customRulesets.forEach((rs) => checker.addRuleset(rs));
return new Promise((resolve, reject) => {
setTimeout(function () {
checker.check(document, policies).then(function (report) {
for (const result of report.results) {
delete result.node;
result.level = valueToLevel(result.value);
}
report.summary ||= {};
report.summary.counts ||= getCounts(report);
// Filter out pass results unless they asked for them in reports
// We don't want to mess with baseline functions, but pass results can break the response object
report.results = report.results.filter(result => reportLevels.includes(result.level) || result.level !== "pass");
resolve(report);
});
}, 0);
});
}, { policies: Config.policies, customRulesets: ACEngineManager.customRulesets, reportLevels: (Config.reportLevels || []).concat(Config.failLevels || []).map(lvl => lvl.toString()) });
if (curPol != null && !checkPolicy) {
const valPolicies = ACEngineManager.customRulesets.map(rs => rs.id).concat(await page.evaluate("new window.ace_ibma.Checker().rulesetIds"));
checkPolicy = true;
areValidPolicy(valPolicies, curPol);
}
let finalReport;
// If there is something to report...
if (report.results) {
let url = await page.evaluate("document.location.href");
let title = await page.evaluate("document.location.title");
let origReport = JSON.parse(JSON.stringify(report));
if (Config.captureScreenshots) {
let image = await page.screenshot({
fullPage: true,
encoding: "base64"
});
origReport.screenshot = image;
}
finalReport = ReporterManager.addEngineReport("Puppeteer", startScan, url, title, label, origReport);
}
page.aceBusy = false;
return {
"report": finalReport,
"puppeteer": parsed
};
}
catch (err) {
console.error(err);
return Promise.reject(err);
}
;
}
async function getComplianceHelperLocal(label, parsed, curPol) {
try {
const valueToLevel = (reportValue) => {
let reportLevel;
if (reportValue[1] === "PASS") {
reportLevel = "pass";
}
else if ((reportValue[0] === "VIOLATION" || reportValue[0] === "RECOMMENDATION") && reportValue[1] === "MANUAL") {
reportLevel = "manual";
}
else if (reportValue[0] === "VIOLATION") {
if (reportValue[1] === "FAIL") {
reportLevel = "violation";
}
else if (reportValue[1] === "POTENTIAL") {
reportLevel = "potentialviolation";
}
}
else if (reportValue[0] === "RECOMMENDATION") {
if (reportValue[1] === "FAIL") {
reportLevel = "recommendation";
}
else if (reportValue[1] === "POTENTIAL") {
reportLevel = "potentialrecommendation";
}
}
return reportLevel;
};
const getCounts = (engineReport) => {
let counts = {
violation: 0,
potentialviolation: 0,
recommendation: 0,
potentialrecommendation: 0,
manual: 0,
pass: 0
};
for (const issue of engineReport.results) {
++counts[issue.level];
}
return counts;
};
let startScan = Date.now();
let checker = ACEngineManager.getChecker();
ACEngineManager.customRulesets.forEach((rs) => checker.addGuideline(rs));
let report = await checker.check(parsed, Config.policies)
.then(function (report) {
for (const result of report.results) {
delete result.node;
result.level = valueToLevel(result.value);
}
report.summary ||= {};
report.summary.counts ||= getCounts(report);
let reportLevels = (Config.reportLevels || []).concat(Config.failLevels || []).map(lvl => lvl.toString());
// Filter out pass results unless they asked for them in reports
// We don't want to mess with baseline functions, but pass results can break the response object
report.results = report.results.filter(result => reportLevels.includes(result.level) || result.level !== "pass");
return report;
});
if (curPol != null && !checkPolicy) {
let valPolicies = checker.getGuidelineIds();
checkPolicy = true;
areValidPolicy(valPolicies, curPol);
}
let finalReport;
// If there is something to report...
if (report.results) {
let url = parsed.location && parsed.location.href;
let origReport = JSON.parse(JSON.stringify(report));
finalReport = ReporterManager.addEngineReport("Native", startScan, url, parsed.title, label, origReport);
}
return {
"report": finalReport
};
}
catch (err) {
console.error(err);
return Promise.reject(err);
}
;
}
//# sourceMappingURL=ACHelper.js.map