diff --git a/kayle/README.md b/kayle/README.md index f3cade20..48798215 100644 --- a/kayle/README.md +++ b/kayle/README.md @@ -89,6 +89,7 @@ kayle supports multiple test runners which return different results. The built-i - `axe`: run tests using [axe-core](./lib/runners/axe.ts). - `htmlcs` (default): run tests using [HTML CodeSniffer](./lib/runners/htmlcs.ts) +- `ace`: run tests using [IBM ACE](https://github.com/IBMa/equal-access/blob/master/accessibility-checker-engine/README.md) - `custom`: custom runners using `injectRunner` util. ## Playwright/Puppeteer diff --git a/kayle/lib/kayle.ts b/kayle/lib/kayle.ts index bff0a081..59728d49 100644 --- a/kayle/lib/kayle.ts +++ b/kayle/lib/kayle.ts @@ -54,10 +54,9 @@ const injectRunners = async (config: RunnerConfig) => { if (!config.browserExtension) { return await Promise.all([ config.page.evaluate(runnersJavascript["kayle"]), - config.page.evaluate(getRunner(config.language, config.runners[0])), - config.runners.length === 2 - ? config.page.evaluate(getRunner(config.language, config.runners[1])) - : undefined, + ...config.runners.map((r) => + config.page.evaluate(getRunner(config.language, r)) + ), ]); } }; diff --git a/kayle/lib/runner-js.ts b/kayle/lib/runner-js.ts index ee6c98b2..4c01d049 100644 --- a/kayle/lib/runner-js.ts +++ b/kayle/lib/runner-js.ts @@ -1,6 +1,7 @@ import { readFileSync } from "fs"; import { axeRunner } from "./runners/axe"; import { htmlcsRunner } from "./runners/htmlcs"; +import { aceRunner } from "./runners/ace"; const loadRunnerFile = (run, langauge) => { if (run === "htmlcs") { @@ -11,6 +12,10 @@ const loadRunnerFile = (run, langauge) => { return axeRunner[langauge || "en"]; } + if (run === "ace") { + return aceRunner[langauge || "en"]; + } + return require(run); }; @@ -52,6 +57,9 @@ const runnersJavascript = { axe_ko: loadRunnerScript("axe", "ko"), // axe_no_NB: loadRunnerScript("axe", "no-NB"), // axe_pt_BR: loadRunnerScript("axe", "pt-BR"), + // expiremental + // ace by IBM + ace: loadRunnerScript("ace", ""), }; // inject a new runner for testing @@ -71,20 +79,10 @@ const getRunner = ( ) => { // if langauge exist get the runner type if (language) { - if (runner === "axe") { - const script = `axe_${language}`; - - if (typeof runnersJavascript[script] !== "undefined") { - return runnersJavascript[script]; - } - } - - if (runner === "htmlcs") { - const script = `htmlcs_${language}`; + const script = `${runner}_${language}`; - if (typeof runnersJavascript[script] !== "undefined") { - return runnersJavascript[script]; - } + if (typeof runnersJavascript[script] !== "undefined") { + return runnersJavascript[script]; } } diff --git a/kayle/lib/runner.ts b/kayle/lib/runner.ts index d810f4f7..b34d0693 100644 --- a/kayle/lib/runner.ts +++ b/kayle/lib/runner.ts @@ -7,6 +7,17 @@ error: 1, warning: 2, notice: 3, + // ibm + VIOLATION: 1, + RECOMMENDATION: 2, + INFORMATION: 3, + }; + + const issueCodeReverseMap = { + 0: "unknown", + 1: "error", + 2: "warning", + 3: "notice", }; // start of code score maps todo: use enums @@ -65,17 +76,34 @@ } } + const typeCode = + issueCodeMap[ + issue.type || + (issue.value && + Array.isArray(issue.value) && + issue.value.length && + issue.value[0]) + ]; + return { - context: context, - selector: selector, - code: issue.code, - type: issue.type, - typeCode: issueCodeMap[issue.type] || 0, + context: context || issue.snippet, + selector: selector || (issue.path && issue.path.dom), + code: issue.code || issue.ruleId, + type: issue.type || issueCodeReverseMap[typeCode], + typeCode: typeCode || 0, message: issue.message, - runner: issue.runner || "kayle", + runner: issue.runner || issue.bounds ? "ace" : "kayle", runnerExtras: issue.runnerExtras, recurrence: issue.recurrence || 0, - clip, + clip: + clip || issue.bounds + ? { + x: issue.bounds.left, + y: issue.bounds.top, + height: issue.bounds.height, + width: issue.bounds.width, + } + : undefined, }; }; @@ -141,7 +169,7 @@ const isIssueNotIgnored = (issue) => { return !options.ignore.some( (element) => - element === issue.type || element === issue.code.toLowerCase() + element === issue.type || element === issue.code?.toLowerCase() ); }; @@ -179,7 +207,12 @@ const validateIssue = (issue) => (options.rootElement && !isElementInTestArea(issue.element)) || (options.hideElements && !isElementOutsideHiddenArea(issue.element)) || - !isIssueNotIgnored(issue); + !isIssueNotIgnored(issue) || + // IBM exclude errors - this check is quick but, we may want to look into conditional logic + (issue.value && + Array.isArray(issue.value) && + issue.value.length >= 2 && + issue.value[1] === "PASS"); // get issues with acc builder return counter const processIssues = ( @@ -270,33 +303,17 @@ // alt elements that require fixing const missingAltIndexs = []; - // multiple runners found - const multiRunners = runnerIssues.length === 2; // pre-allocate array if multi runners - const issues = new Array( - multiRunners - ? runnerIssues[0].length + runnerIssues[1].length - : runnerIssues[0].length - ); + const issues = new Array(runnerIssues.reduce((r, x) => r + x.length, 0)); const tracker = { errorPointer: 0, ic: 0, }; - // init index for runner - processIssues( - runnerIssues[0], - issues, - tracker, // init index - meta, - missingAltIndexs - ); - - // process second runner if found - if (multiRunners) { - processIssues(runnerIssues[1], issues, tracker, meta, missingAltIndexs); + for (const runnerIssue of runnerIssues) { + processIssues(runnerIssue, issues, tracker, meta, missingAltIndexs); } issues.length = tracker.ic; diff --git a/kayle/lib/runners/ace.ts b/kayle/lib/runners/ace.ts new file mode 100644 index 00000000..67aa2d51 --- /dev/null +++ b/kayle/lib/runners/ace.ts @@ -0,0 +1,53 @@ +const run = async (options) => { + return new Promise((resolve, reject) => { + if ( + // @ts-ignore + typeof window.define === "function" && + // @ts-ignore + window.define.amd && + typeof window.require === "function" + ) { + // @ts-ignore module load + window.require(["accessibility-checker-engine"], (ace) => { + Object.keys(ace).forEach((key) => { + window[key] = ace[key]; + }); + // @ts-ignore set the window runner + window.ace = ace; + }); + } + + // @ts-ignore + const checker = new ace.Checker(); + + checker + .check( + (options.rootElement && + window.document.querySelector(options.rootElement)) || + window.document, + ["IBM_Accessibility"] + ) + .then((report) => { + resolve(report.results); + }); + }); +}; + +const aceRunner = { + en: { + scripts: [require.resolve("accessibility-checker-engine/ace.js")], + run, + }, +}; + +// // hard code locales to the list to htmlcs locales not EN +// const locales = ["fr", "es", "it", "ja", "nl", "pl", "zh_CN", "zh_TW"]; + +// for (const lang of locales) { +// ibmRunner[lang.replace("_", "-")] = { +// scripts: [require.resolve(`fast_htmlcs/build/HTMLCS.${lang}.js`)], +// run, +// }; +// } + +export { aceRunner }; diff --git a/kayle/package.json b/kayle/package.json index 0f9ec560..b9aa6c3f 100644 --- a/kayle/package.json +++ b/kayle/package.json @@ -1,6 +1,6 @@ { "name": "kayle", - "version": "0.6.4", + "version": "0.7.0", "description": "Extremely fast and accurate accessibility engine built for any headless tool like playwright or puppeteer.", "main": "./build/index.js", "keywords": [ @@ -35,6 +35,7 @@ "test:puppeteer": "npm run compile:test && node _tests/tests/basic.js", "test:puppeteer:axe": "npm run compile:test && node _tests/tests/basic-axe.js", "test:puppeteer:htmlcs": "npm run compile:test && node _tests/tests/basic-htmlcs.js", + "test:puppeteer:ace": "npm run compile:test && node _tests/tests/basic-ace.js", "test:missing": "npm run compile:test && node _tests/tests/missing.js", "test:playwright": "npm run compile:test && npx playwright test ./tests/basic-playwright.spec.ts", "test:playwright:axe": "npm run compile:test && npx playwright test ./tests/basic-axe-playwright.spec.ts", @@ -57,6 +58,7 @@ "bugs": "https://github.com/a11ywatch/kayle/issues", "license": "MIT", "dependencies": { + "accessibility-checker-engine": "3.1.61", "fast_axecore": "workspace:*", "fast_htmlcs": "workspace:*", "kayle_innate": "workspace:*" diff --git a/kayle/tests/basic-ace.ts b/kayle/tests/basic-ace.ts new file mode 100644 index 00000000..5e1dc558 --- /dev/null +++ b/kayle/tests/basic-ace.ts @@ -0,0 +1,40 @@ +import assert from "assert"; +import puppeteer from "puppeteer"; +import { kayle } from "kayle"; +import { drakeMock } from "./mocks/html-mock"; +import { performance } from "perf_hooks"; + +(async () => { + const browser = await puppeteer.launch({ headless: "new" }); + const page = await browser.newPage(); + if (process.env.LOG_ENABLED) { + page.on("console", (msg) => console.log("PAGE LOG:", msg.text())); + } + const startTime = performance.now(); + const { issues, pageUrl, documentTitle, meta, automateable } = await kayle({ + page, + browser, + runners: ["ace"], + includeWarnings: true, + html: drakeMock, + origin: "https://www.drake.com", // origin is the fake url in place of the raw content + }); + const nextTime = performance.now() - startTime; + + console.log(issues); + console.log(meta); + console.log(automateable); + console.log("time took", nextTime); + + // valid list + assert(Array.isArray(issues)); + assert(meta.errorCount === 55); + assert(meta.warningCount === 42); + // TODO: IBM ACE accessibility error handling + // assert(meta.accessScore === 50); + + assert(typeof pageUrl === "string"); + assert(typeof documentTitle === "string"); + + await browser.close(); +})(); diff --git a/yarn.lock b/yarn.lock index c808b79f..bdcf0bc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2100,6 +2100,13 @@ __metadata: languageName: node linkType: hard +"accessibility-checker-engine@npm:3.1.61": + version: 3.1.61 + resolution: "accessibility-checker-engine@npm:3.1.61" + checksum: 08d5f4138c242e74a4516aee56c9b6550589d91ebce58873e38fb9272674e7fbb132c3bbbe69a71fc9b4fb445773e2dd57154e6a21b78574f0f19a10caf0c94a + languageName: node + linkType: hard + "acorn-jsx@npm:^5.0.0, acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -6104,6 +6111,7 @@ __metadata: "@swc/helpers": ^0.5.1 "@types/jsdom": ^20.0.1 "@types/node": ^18.11.9 + accessibility-checker-engine: 3.1.61 fast_axecore: "workspace:*" fast_htmlcs: "workspace:*" kayle_innate: "workspace:*"