diff --git a/README.md b/README.md index 9d66aefb..08ec06a4 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ const results = await kayle({ - [`fast_axecore`](./fast_htmlcs/README.md): run tests using fork of [axe-core](./lib/runners/axe.ts). - [`fast_htmlcs`](./fast_htmlcs/README.md): run tests using fork of [HTML CodeSniffer](./lib/runners/htmlcs.ts). -- `custom`: custom runners using `injectRunner` util. +- `custom`: custom runners using [`injectRunner`](./kayle/lib/runner-js.ts#l=57) util - library authors. ## Linting @@ -245,6 +245,42 @@ Use the command `yarn build` to compile all the scripts for each locale. We are building a rust based runner called [kayle_innate](./kayle_innate/) that can port to wasm that will take the audits into the nanoseconds - low milliseconds zone. +## Custom Runner (library authors) + +Below is an example on adding a new custom runner. Take a look at [HTMLCS](fast_htmlcs/HTMLCS.ts) and [HTMLCS_Runner](kayle/lib/runners/htmlcs.ts) setup for an example of how to setup the scripts, it could take around 10 mins to get familiar. You can also overwride the base runners by taking the `runnersJavascript` object and appending to the script. + +```ts +import { injectRunner, kayle } from "kayle" + +// example of the custom script take a +injectRunner("htmlcs_extended", "./custom_htmlcs_script", "en") + +const results = await kayle({ + page, + browser, + runners: ["htmlcs", "htmlcs_extended"] + origin: "https://a11ywatch.com", +}); +``` + +## Extend Runner + +Extending a runner can be done with the following. + +```ts +import { extendRunner, kayle } from "kayle" + +// example of the custom script take a +extendRunner("htmlcs", "./custom_htmlcs_script", "en") + +const results = await kayle({ + page, + browser, + runners: ["htmlcs", "htmlcs_extended"] + origin: "https://a11ywatch.com", +}); +``` + ## Discord If you want to chat about the project checkout our [Discord][discord-url]. diff --git a/fast_htmlcs/HTMLCS.ts b/fast_htmlcs/HTMLCS.ts index eb73ec46..aff5b0f0 100755 --- a/fast_htmlcs/HTMLCS.ts +++ b/fast_htmlcs/HTMLCS.ts @@ -340,23 +340,22 @@ _global.HTMLCS = new (function () { failCallback, options ) => { - const _standard = _getStandardPath(standard); - + const localStandard = _getStandardPath(standard); // See if the ruleset object is already included (eg. if minified). - const parts = _standard.split("/"); + const parts = localStandard.split("/"); const part = parts[parts.length - 2]; const ruleSet = _getRuleset(part); if (ruleSet) { // Already included. - _registerStandard(_standard, part, callback, failCallback, options); + this.registerStandard(standard, part, callback, failCallback, options); } else { // TODO: remove _include script callback standard always included _includeScript( _standard, function () { // Script is included now register the standard. - _registerStandard(_standard, part, callback, failCallback, options); + this.registerStandard(_standard, part, callback, failCallback, options); }, failCallback ); @@ -370,7 +369,7 @@ _global.HTMLCS = new (function () { * @param {Function} callback The function to call once the standard is registered. * @param {Object} options The options for the standard (e.g. exclude sniffs). */ - const _registerStandard = ( + this.registerStandard = ( standard, part, callback, @@ -390,6 +389,7 @@ _global.HTMLCS = new (function () { } } + // include the new rules for the standard _standards.set(standard, ruleSet); // Process the options. @@ -456,8 +456,8 @@ _global.HTMLCS = new (function () { if (typeof sniff === "string") { const sniffObj = _getSniff(standard, sniff); - const cb = function () { - _registerSniff(standard, sniff); + const cb = () => { + this.registerSniff(standard, sniff); callback.call(this); }; @@ -490,7 +490,7 @@ _global.HTMLCS = new (function () { * @param {String} standard The name of the standard. * @param {String} sniff The name of the sniff. */ - const _registerSniff = (standard, sniff) => { + this.registerSniff = (standard: string, sniff: string) => { // Get the sniff object. const sniffObj = _getSniff(standard, sniff); @@ -542,20 +542,17 @@ _global.HTMLCS = new (function () { * * @returns {Object} The sniff object. */ - const _getSniff = (standard, sniff) => { - const cstandard = _standards.has(standard) && _standards.get(standard); // standard should always exist - let name = "HTMLCS_"; - - name += ((cstandard && cstandard.name) || "") + "_Sniffs_"; - name += sniff.split(".").join("_"); + const _getSniff = (standard, sniff: string) => { + const name = `HTMLCS_${standard}_Sniffs_${sniff.split(".").join("_")}`; + const sniffSet = _global[name] || window[name]; - if (!_global[name]) { + if (!sniffSet) { return null; } + + sniffSet._name = sniff; - _global[name]._name = sniff; - - return _global[name]; + return sniffSet; }; /** diff --git a/fast_htmlcs/Standards/WCAG2AAA/Sniffs/Principle3/Guideline3_1/3_1_1.ts b/fast_htmlcs/Standards/WCAG2AAA/Sniffs/Principle3/Guideline3_1/3_1_1.ts index ebdd1c91..5e20660e 100644 --- a/fast_htmlcs/Standards/WCAG2AAA/Sniffs/Principle3/Guideline3_1/3_1_1.ts +++ b/fast_htmlcs/Standards/WCAG2AAA/Sniffs/Principle3/Guideline3_1/3_1_1.ts @@ -29,28 +29,28 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle3_Guideline3_1_3_1_1 = { "H57.2" ); } else { - if (element.hasAttribute("lang") === true) { - var lang = element.getAttribute("lang"); - if (this.isValidLanguageTag(lang) === false) { - HTMLCS.addMessage( - HTMLCS.ERROR, - top, - _global.HTMLCS.getTranslation("3_1_1_H57.3.Lang"), - "H57.3.Lang" - ); - } + if ( + element.hasAttribute("lang") && + !this.isValidLanguageTag(element.getAttribute("lang")) + ) { + HTMLCS.addMessage( + HTMLCS.ERROR, + top, + _global.HTMLCS.getTranslation("3_1_1_H57.3.Lang"), + "H57.3.Lang" + ); } - if (element.hasAttribute("xml:lang") === true) { - var lang = element.getAttribute("xml:lang"); - if (this.isValidLanguageTag(lang) === false) { - HTMLCS.addMessage( - HTMLCS.ERROR, - top, - _global.HTMLCS.getTranslation("3_1_1_H57.3.XmlLang"), - "H57.3.XmlLang" - ); - } + if ( + element.hasAttribute("xml:lang") && + !this.isValidLanguageTag(element.getAttribute("xml:lang")) + ) { + HTMLCS.addMessage( + HTMLCS.ERROR, + top, + _global.HTMLCS.getTranslation("3_1_1_H57.3.XmlLang"), + "H57.3.XmlLang" + ); } } }, @@ -97,16 +97,8 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle3_Guideline3_1_3_1_1 = { regexStr += "(-x(-[a-z0-9]{1,8})+)?$"; // Make a regex out of it, and make it all case-insensitive. - var regex = new RegExp(regexStr, "i"); - // Throw the correct lang code depending on whether this is a document // element or not. - var valid = true; - - if (regex.test(langTag) === false) { - valid = false; - } - - return valid; + return new RegExp(regexStr, "i").test(langTag); }, }; diff --git a/fast_htmlcs/Standards/WCAG2AAA/Sniffs/Principle3/Guideline3_1/3_1_2.ts b/fast_htmlcs/Standards/WCAG2AAA/Sniffs/Principle3/Guideline3_1/3_1_2.ts index 2d1d2399..f8c37ea1 100644 --- a/fast_htmlcs/Standards/WCAG2AAA/Sniffs/Principle3/Guideline3_1/3_1_2.ts +++ b/fast_htmlcs/Standards/WCAG2AAA/Sniffs/Principle3/Guideline3_1/3_1_2.ts @@ -35,7 +35,7 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle3_Guideline3_1_3_1_2 = { let langEl = null; - for (var i = 0; i <= langEls.length; i++) { + for (let i = 0; i <= langEls.length; i++) { if (i === langEls.length) { langEl = top; } else { diff --git a/fast_htmlcs/package.json b/fast_htmlcs/package.json index 4a542424..fa487324 100644 --- a/fast_htmlcs/package.json +++ b/fast_htmlcs/package.json @@ -1,6 +1,6 @@ { "name": "fast_htmlcs", - "version": "0.0.72", + "version": "0.0.74", "description": "A high performance fork of HTML_CodeSniffer.", "license": "BSD-3-Clause", "main": "index.js", diff --git a/kayle/lib/config.ts b/kayle/lib/config.ts index 0ab55dd2..6b2bea91 100644 --- a/kayle/lib/config.ts +++ b/kayle/lib/config.ts @@ -197,3 +197,8 @@ export enum Standard { } export { Runner }; + +export const enum MainRunner { + htmlcs = "htmlcs", + axe = "axe", +} diff --git a/kayle/lib/index.ts b/kayle/lib/index.ts index 2cd2ca2d..a62ce2c8 100644 --- a/kayle/lib/index.ts +++ b/kayle/lib/index.ts @@ -1,7 +1,12 @@ export { kayle } from "./kayle"; export type { Issue, Audit, MetaInfo, Automatable } from "./common"; export { autoKayle } from "./auto"; -export { runnersJavascript, getRunner, injectRunner } from "./runner-js"; +export { + runnersJavascript, + getRunner, + injectRunner, + extendRunner, +} from "./runner-js"; export { goToPage, setNetworkInterception, @@ -14,6 +19,7 @@ export { export { setLogging, Standard, + MainRunner, type RunnerConfig, type Runner, type LifeCycleEvent, diff --git a/kayle/lib/runner-js.ts b/kayle/lib/runner-js.ts index 06fddbe2..74d926a5 100644 --- a/kayle/lib/runner-js.ts +++ b/kayle/lib/runner-js.ts @@ -54,7 +54,13 @@ const runnersJavascript = { // axe_pt_BR: loadRunnerScript("axe", "pt-BR"), }; -// inject a new runner for testing +/** + * Inject a new runner for testing + * @param {String} [runner="custom_name"] - The custom runner name. + * @param {String} [path=""] - The path of the file for the runner script. + * @param {String} [lang=""] - The language of the runner script. + * @returns {void} + */ const injectRunner = (runner: string, path: string, lang: string) => { runnersJavascript[runner] = loadRunnerScript(path, lang ?? ""); }; @@ -102,4 +108,18 @@ export type Runner = Exclude< | "axe_ko" >; -export { runnersJavascript, getRunner, injectRunner }; +/** + * Extend the base of a runner with custom code. + * @param {String} [runner=""] - The custom runner to extend. + * @param {String} [script=""] - The custom javascript. + * @param {String} [lang=""] - The target langauge for the script. + * @returns {void} + */ +const extendRunner = (runner: Runner, script: string, lang?: string) => { + const runnerType = `${runner}${lang ? `_${lang}` : ""}`; + const runnerCode = runnersJavascript[runnerType]; + + runnersJavascript[runnerType] = `${runnerCode}${script};`; +}; + +export { runnersJavascript, getRunner, injectRunner, extendRunner }; diff --git a/kayle/lib/runner.ts b/kayle/lib/runner.ts index d54d3fcf..4c158397 100644 --- a/kayle/lib/runner.ts +++ b/kayle/lib/runner.ts @@ -60,10 +60,7 @@ if (issue.element) { context = getElementContext(issue.element); selector = getElementSelector(issue.element); - if ( - cliped && - typeof issue.element.getBoundingClientRect === "function" - ) { + if (cliped && typeof issue.element.getBoundingClientRect === "function") { const { x, y, width, height } = issue.element.getBoundingClientRect(); clip = { diff --git a/kayle/package.json b/kayle/package.json index 7bbd8d3c..e23159dc 100644 --- a/kayle/package.json +++ b/kayle/package.json @@ -1,6 +1,6 @@ { "name": "kayle", - "version": "0.8.11", + "version": "0.8.12", "description": "Extremely fast and accurate accessibility engine built for any headless tool like playwright or puppeteer.", "main": "./build/index.js", "keywords": [ @@ -53,6 +53,7 @@ "test:puppeteer:tables": "npm run compile:test && node _tests/tests/tables.js", "test:puppeteer:clips": "npm run compile:test && node _tests/tests/clips.js", "test:puppeteer:innate": "npm run compile:test && node _tests/tests/innate.js", + "test:htmlcs:extend": "npm run compile:test && node _tests/tests/extend-runner.js", "test:full": "npm run compile:test && node _tests/tests/full.js", "test:lint": "node build/lint.js", "test:unit:unique-selector": "npm run compile:test && node _tests/tests/unit/unique-selector.js", diff --git a/kayle/tests/basic-htmlcs-playwright.spec.ts b/kayle/tests/basic-htmlcs-playwright.spec.ts index 6cefaa34..75c9e6b7 100644 --- a/kayle/tests/basic-htmlcs-playwright.spec.ts +++ b/kayle/tests/basic-htmlcs-playwright.spec.ts @@ -17,7 +17,7 @@ test("fast_htmlcs audit drakeMock", async ({ page, browser }, testInfo) => { includeWarnings: true, html: drakeMock, origin: "https://www.drake.com", - standard: Standard.WCAG2AA + standard: Standard.WCAG2AA, }); const endTime = performance.now() - startTime; diff --git a/kayle/tests/extend-runner.ts b/kayle/tests/extend-runner.ts new file mode 100644 index 00000000..618f8613 --- /dev/null +++ b/kayle/tests/extend-runner.ts @@ -0,0 +1,85 @@ +import assert from "assert"; +import puppeteer from "puppeteer"; +import { Standard, kayle, extendRunner, MainRunner } 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(); + + // pure javascript required. No typescript! + extendRunner( + MainRunner.htmlcs, + ` + // use console.log(JSON.stringify(Object.keys(window))) to see all of the objects to extend. + // set the function HTMLCS_WCAG2AAA_Sniffs_Principle2_Guideline2_4_2_4_2.process to a variable to re-use the logic prior in the call. + + // store the prior sniff in a variable to re-use the logic + const prevHeadSniffCase = HTMLCS_WCAG2AAA_Sniffs_Principle2_Guideline2_4_2_4_2.process; + + HTMLCS_WCAG2AAA_Sniffs_Principle2_Guideline2_4_2_4_2.process = (element, _) => { + // re-run the logic for the case + prevHeadSniffCase(element, _); + // log something to test if output ran + console.log("Running extended head element case"); + // we can write a test here that should pass some logic. For now we just add a new error + HTMLCS.addMessage( + HTMLCS.ERROR, + element, + HTMLCS.getTranslation("2_4_2_H25.1.NoHeadEl"), + "H25.1.NoHeadEl" + ); + } + + // Add a new rule example - 4_1_4_1_4 + + window["HTMLCS_WCAG2AAA_Sniffs_Principle4_Guideline4_1_4_1_4"] = { + register: () => ["html"], + process: (element, _) => { + console.log("NEW Rule run!"); + HTMLCS.addMessage( + HTMLCS.ERROR, + element, + "This is some new rule for something.", + "H55.1.NoItem" + ); + }, + }; + + // push the new sniff to the list + HTMLCS_WCAG2AAA.sniffs.push("Principle4.Guideline4_1.4_1_4"); + // register the new sniff rule to run + HTMLCS.registerSniff("WCAG2AAA", "Principle4.Guideline4_1.4_1_4"); + `.trimStart() + ); + + const { issues, pageUrl, documentTitle, meta, automateable } = await kayle({ + page, + browser, + runners: [MainRunner.htmlcs], + includeWarnings: true, + standard: Standard.WCAG2AAA, + origin: "https://www.drake.com", + html: drakeMock, + }); + const nextTime = performance.now() - startTime; + + console.log(`Issue count ${issues.length}`); + console.log(meta); + console.log(automateable); + console.log("time took", nextTime); + + // valid list + assert(Array.isArray(issues)); + assert(typeof pageUrl === "string"); + assert(typeof documentTitle === "string"); + // we should have two extra errors + assert(meta.errorCount == 25); + + await browser.close(); +})();