From ce3c3731ff96536ca765aefcd539053cf4ecb112 Mon Sep 17 00:00:00 2001 From: j-mendez Date: Fri, 8 Mar 2024 17:55:02 -0500 Subject: [PATCH] feat(rules): add pre-compiled rules --- .gitignore | 1 + fast_htmlcs/HTMLCS.ts | 10 - kayle/.gitignore | 1 - kayle/bench/fast_axecore-playwright.ts | 2 +- kayle/bench/fast_htmlcs-playwright.ts | 2 +- kayle/build-extension.ts | 2 +- kayle/build-rules.ts | 94 ++ kayle/lib/action.ts | 22 +- kayle/lib/auto.ts | 8 +- kayle/lib/config.ts | 10 +- kayle/lib/index.ts | 2 + kayle/lib/kayle.ts | 16 +- kayle/lib/lint.ts | 4 +- kayle/lib/rules/axe-rules.ts | 996 ++++++++++++++++++++ kayle/lib/rules/htmlcs-rules.ts | 83 ++ kayle/lib/runner-js.ts | 2 +- kayle/lib/runner.ts | 12 +- kayle/lib/runners/axe.ts | 4 +- kayle/lib/runners/htmlcs.ts | 4 +- kayle/lib/utils/adblock.ts | 4 +- kayle/lib/utils/cdp-blocking.ts | 6 +- kayle/lib/utils/go-to-page.ts | 8 +- kayle/lib/utils/resource-ignore.ts | 2 +- kayle/lib/wasm/extract.ts | 2 +- kayle/lib/watcher.ts | 2 +- kayle/package.json | 8 +- kayle/tests/basic-axe-playwright.spec.ts | 6 +- kayle/tests/basic-htmlcs-playwright.spec.ts | 6 +- kayle/tests/basic-playwright.spec.ts | 6 +- kayle/tests/clips-playwright.spec.ts | 6 +- kayle/tests/extend-runner.ts | 2 +- kayle/tests/extension.ts | 2 +- kayle/tests/i18n.ts | 2 +- kayle/tests/innate-playwright.spec.ts | 2 +- kayle/tests/innate.ts | 4 +- kayle/tests/wasm.ts | 2 +- yarn.lock | 13 +- 37 files changed, 1267 insertions(+), 91 deletions(-) create mode 100644 kayle/build-rules.ts create mode 100644 kayle/lib/rules/axe-rules.ts create mode 100644 kayle/lib/rules/htmlcs-rules.ts diff --git a/.gitignore b/.gitignore index ebcabd95..d4005cfc 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ chrome-extension # build custom extension build-extension.js +build-rules.js # data dir _data \ No newline at end of file diff --git a/fast_htmlcs/HTMLCS.ts b/fast_htmlcs/HTMLCS.ts index a0f2b943..2f031fcb 100755 --- a/fast_htmlcs/HTMLCS.ts +++ b/fast_htmlcs/HTMLCS.ts @@ -237,16 +237,6 @@ _global.HTMLCS = new (function () { } }; - /** - * Returns all the messages for the last run. - * - * Return a copy of the array so the class variable doesn't get modified by - * future modification (eg. splicing). - * - * @returns {Array} Array of message objects. - */ - this.getMessages = () => this.messages; - /** * Runs the sniffs in the loaded standard for the specified element. * diff --git a/kayle/.gitignore b/kayle/.gitignore index 8725d527..2ac8bec6 100644 --- a/kayle/.gitignore +++ b/kayle/.gitignore @@ -8,5 +8,4 @@ build _tests ./test-results _data - ./screenshot.png \ No newline at end of file diff --git a/kayle/bench/fast_axecore-playwright.ts b/kayle/bench/fast_axecore-playwright.ts index 9f2707e4..bace8587 100644 --- a/kayle/bench/fast_axecore-playwright.ts +++ b/kayle/bench/fast_axecore-playwright.ts @@ -53,7 +53,7 @@ async function launchBench() { unit: "OPS/S", value: 1000 / avg, }, - ]) + ]), ); } diff --git a/kayle/bench/fast_htmlcs-playwright.ts b/kayle/bench/fast_htmlcs-playwright.ts index fc495d85..08925d20 100644 --- a/kayle/bench/fast_htmlcs-playwright.ts +++ b/kayle/bench/fast_htmlcs-playwright.ts @@ -53,7 +53,7 @@ async function launchBench() { unit: "OPS/S", value: 1000 / avg, }, - ]) + ]), ); } diff --git a/kayle/build-extension.ts b/kayle/build-extension.ts index 7fc9f616..bcbdf4cf 100644 --- a/kayle/build-extension.ts +++ b/kayle/build-extension.ts @@ -52,7 +52,7 @@ window.addEventListener("kayle_send", async (event) => { writeFileSync( `${ext}/content-script.js`, - `${extensionRunner}\n${extensionAxe}\n${extensionHtmlcs}\n${extensionRawEnd}` + `${extensionRunner}\n${extensionAxe}\n${extensionHtmlcs}\n${extensionRawEnd}`, ); const extensionManifest = `{ diff --git a/kayle/build-rules.ts b/kayle/build-rules.ts new file mode 100644 index 00000000..a8284b87 --- /dev/null +++ b/kayle/build-rules.ts @@ -0,0 +1,94 @@ +import { chromium } from "playwright"; +import { Standard, kayle } from "kayle"; +import { writeFile } from "fs/promises"; +import { format } from "prettier"; + +type Rule = { + ruleId: string; + description?: string; + help?: string; + helpUrl?: string; + tags?: string[]; + actIds?: string[]; +}; + +(async () => { + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage(); + + const fast_htmlcs_rules: Rule[] = []; + const fast_axe_rules: Rule[] = []; + + // inject the scripts + await kayle( + { + page, + browser, + runners: ["htmlcs", "axe"], + html: ">

Build Rules list

", + standard: Standard.WCAG2AA, + origin: "https://www.example.com", + }, + true, + ); + + await page.exposeFunction("pushHtmlcsRule", (t: Rule) => { + fast_htmlcs_rules.push(t); + }); + + await page.exposeFunction("pushAxeRule", (t: Rule) => { + fast_axe_rules.push(t); + }); + + await page.evaluate(() => { + // @ts-ignore + for (const r of window.axe.getRules()) { + // @ts-ignore we need to get the rules description and urls. + window.pushAxeRule(r); + } + for (const k of Object.keys(window)) { + if (k.startsWith("HTMLCS_WCAG2AAA_Sniffs_Principle")) { + // @ts-ignore + window.pushHtmlcsRule({ + ruleId: k, + }); + } + } + }); + + const pConfig = { + singleQuote: true, + semi: false, + parser: "babel", + }; + + const DNE = "THIS FILE WAS CREATED DYNAMICALLY - DO NOT EDIT"; + + await writeFile( + "./lib/rules/htmlcs-rules.ts", + Buffer.from( + await format( + `/* ${DNE} */\nexport const htmlcsRules = ${JSON.stringify( + fast_htmlcs_rules, + )};`, + pConfig, + ), + ), + "utf8", + ); + + await writeFile( + "./lib/rules/axe-rules.ts", + Buffer.from( + await format( + `/* ${DNE} */\nexport const axeRules = ${JSON.stringify( + fast_axe_rules, + )};`, + pConfig, + ), + ), + "utf8", + ); + + await browser.close(); +})(); diff --git a/kayle/lib/action.ts b/kayle/lib/action.ts index e336375a..909222c7 100644 --- a/kayle/lib/action.ts +++ b/kayle/lib/action.ts @@ -56,13 +56,13 @@ export const actions = [ target.dispatchEvent( new Event("input", { bubbles: true, - }) + }), ); return Promise.resolve(); }, selector, - value + value, ); } catch (error) { throw new Error(`${failedActionElement} "${selector}"`); @@ -91,7 +91,7 @@ export const actions = [ target.dispatchEvent( new Event("input", { bubbles: true, - }) + }), ); return Promise.resolve(); }, selector); @@ -117,12 +117,12 @@ export const actions = [ target.dispatchEvent( new Event("change", { bubbles: true, - }) + }), ); return Promise.resolve(); }, selector, - checked + checked, ); } catch (error) { throw new Error(`${failedActionElement} "${selector}"`); @@ -176,7 +176,7 @@ export const actions = [ {}, property, expectedValue, - negated + negated, ); }, }, @@ -216,7 +216,7 @@ export const actions = [ polling: 200, }, selector, - state + state, ); }, }, @@ -244,11 +244,11 @@ export const actions = [ }, { once: true, - } + }, ); }, selector, - eventType + eventType, ); await page.waitForFunction( @@ -263,7 +263,7 @@ export const actions = [ }, { polling: 200, - } + }, ); } catch (error) { throw new Error(`${failedActionElement} "${selector}"`); @@ -283,7 +283,7 @@ export const actions = [ */ export async function runAction(browser, page, options, act, customActions?) { const action = (customActions ?? actions).find((item) => - item.match.test(act) + item.match.test(act), ); if (!action) { diff --git a/kayle/lib/auto.ts b/kayle/lib/auto.ts index e92df798..dcde82d9 100644 --- a/kayle/lib/auto.ts +++ b/kayle/lib/auto.ts @@ -17,7 +17,7 @@ export async function autoKayle( cb?: ((result: Audit) => Promise) | ((result: Audit) => void); } = {}, ignoreSet?: Set, - _results?: Audit[] + _results?: Audit[], ): Promise { if (!write) { const { writeFile } = await import("fs/promises"); @@ -51,7 +51,7 @@ export async function autoKayle( if (o.store) { await write( `${o.store}/${encodeURIComponent(o.page.url())}`, - await o.page.content() + await o.page.content(), ); } @@ -84,7 +84,7 @@ export async function autoKayle( origin: link, }, ignoreSet, - _results + _results, ); }) .catch((e) => { @@ -94,7 +94,7 @@ export async function autoKayle( console.error(e); } }); - }) + }), ); return _results; diff --git a/kayle/lib/config.ts b/kayle/lib/config.ts index e51ab9b4..020b7dbd 100644 --- a/kayle/lib/config.ts +++ b/kayle/lib/config.ts @@ -42,7 +42,7 @@ type BrowserContext = { newCDPSession?(page: Partial | Frame): Partial; overridePermissions?( origin: string, - permissions: Permission[] + permissions: Permission[], ): Promise; }; @@ -89,7 +89,7 @@ type Page = { _routes?: { url: string }[]; route( path: string, - intercept: (config: any, next: any) => Promise | Promise + intercept: (config: any, next: any) => Promise | Promise, ): Promise; setRequestInterception?(enable?: boolean): Promise; listenerCount?(name: string): number; @@ -99,19 +99,19 @@ type Page = { | Function | { default: Function; - } + }, ): Promise; addInitScript?(script: { content?: string }): Promise; evaluateOnNewDocument?< Params extends unknown[], - Func extends (...args: Params) => unknown = (...args: Params) => unknown + Func extends (...args: Params) => unknown = (...args: Params) => unknown, >( pageFunction: Func | string, ...args: Params ): Promise<{ identifier: string }>; evaluate< Params extends unknown[], - Func extends EvaluateFunc = EvaluateFunc + Func extends EvaluateFunc = EvaluateFunc, >( pageFunction: Func | string, ...args: Params diff --git a/kayle/lib/index.ts b/kayle/lib/index.ts index a62ce2c8..f23b72ab 100644 --- a/kayle/lib/index.ts +++ b/kayle/lib/index.ts @@ -26,3 +26,5 @@ export { type WaitForOptions, } from "./config"; export { extractLinks, innateBuilder } from "./wasm"; +export { htmlcsRules } from "./rules/htmlcs-rules"; +export { axeRules } from "./rules/axe-rules"; diff --git a/kayle/lib/kayle.ts b/kayle/lib/kayle.ts index b6864277..f4a2b6fa 100644 --- a/kayle/lib/kayle.ts +++ b/kayle/lib/kayle.ts @@ -32,7 +32,7 @@ const audit = async (config: RunnerConfig): Promise => { origin: config.origin, language: config.language, clip: config.clip, - } + }, ); }; @@ -61,7 +61,7 @@ const injectRunners = async (config: RunnerConfig) => { return await Promise.allSettled([ config.page.evaluate(runnersJavascript["kayle"]), ...config.runners.map((r) => - config.page.evaluate(getRunner(config.language, r)) + config.page.evaluate(getRunner(config.language, r)), ), ]); } @@ -77,7 +77,7 @@ export const auditExtension = async (config: RunnerConfig): Promise => { } window.addEventListener("kayle_receive", (event: CustomEvent) => - resolve(event.detail.data) + resolve(event.detail.data), ); window.dispatchEvent( @@ -86,7 +86,7 @@ export const auditExtension = async (config: RunnerConfig): Promise => { name: "kayle", options: runOptions, }, - }) + }), ); }); }, @@ -100,7 +100,7 @@ export const auditExtension = async (config: RunnerConfig): Promise => { origin: config.origin, language: config.language, clip: config.clip, - } + }, ); }; @@ -121,7 +121,7 @@ const auditPageInnate = async (config: RunnerConf, results: Audit) => { const innateAudit: InnateIssue[] = await kayle_innate.audit( html, css, - config.clip + config.clip, ); for (const innateIssue of innateAudit) { @@ -163,7 +163,7 @@ const auditPageInnate = async (config: RunnerConf, results: Audit) => { */ export const kayle = async ( o: RunnerConf = {}, - preventClose?: boolean + preventClose?: boolean, ): Promise => { const watcher = new Watcher(); const navigate = o.page.url() === "about:blank" && (o.origin || o.html); @@ -212,7 +212,7 @@ export const kayle = async ( } return item; - }) + }), ); } diff --git a/kayle/lib/lint.ts b/kayle/lib/lint.ts index d8c36e36..2b420f1a 100644 --- a/kayle/lib/lint.ts +++ b/kayle/lib/lint.ts @@ -12,7 +12,7 @@ export const kayleLint = async ( source?: string, // url or html source o: Partial = {}, runner?: keyof typeof runnersJavascript, - forward?: boolean // forward messages to console + forward?: boolean, // forward messages to console ) => { const config = extractArgs(o); let html = source; @@ -55,7 +55,7 @@ export const kayleLint = async ( dom.window.eval(runnersJavascript.kayle); dom.window.eval( - runner ? runnersJavascript[runner] : runnersJavascript["htmlcs"] + runner ? runnersJavascript[runner] : runnersJavascript["htmlcs"], ); const results = await dom.window.__a11y.run({ diff --git a/kayle/lib/rules/axe-rules.ts b/kayle/lib/rules/axe-rules.ts new file mode 100644 index 00000000..268518a6 --- /dev/null +++ b/kayle/lib/rules/axe-rules.ts @@ -0,0 +1,996 @@ +/* THIS FILE WAS CREATED DYNAMICALLY - DO NOT EDIT */ +export const axeRules = [ + { + ruleId: 'accesskeys', + description: 'Ensures every accesskey attribute value is unique', + help: 'accesskey attribute value should be unique', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/accesskeys?application=axeAPI', + tags: ['cat.keyboard', 'best-practice'], + }, + { + ruleId: 'area-alt', + description: 'Ensures elements of image maps have alternate text', + help: 'Active elements must have alternate text', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/area-alt?application=axeAPI', + tags: [ + 'cat.text-alternatives', + 'wcag2a', + 'wcag244', + 'wcag412', + 'section508', + 'section508.22.a', + 'ACT', + ], + actIds: ['c487ae'], + }, + { + ruleId: 'aria-allowed-attr', + description: "Ensures ARIA attributes are allowed for an element's role", + help: 'Elements must only use allowed ARIA attributes', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-allowed-attr?application=axeAPI', + tags: ['cat.aria', 'wcag2a', 'wcag412'], + actIds: ['5c01ea'], + }, + { + ruleId: 'aria-allowed-role', + description: + 'Ensures role attribute has an appropriate value for the element', + help: 'ARIA role should be appropriate for the element', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-allowed-role?application=axeAPI', + tags: ['cat.aria', 'best-practice'], + }, + { + ruleId: 'aria-command-name', + description: + 'Ensures every ARIA button, link and menuitem has an accessible name', + help: 'ARIA commands must have an accessible name', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-command-name?application=axeAPI', + tags: ['cat.aria', 'wcag2a', 'wcag412', 'ACT'], + actIds: ['97a4e1'], + }, + { + ruleId: 'aria-dialog-name', + description: + 'Ensures every ARIA dialog and alertdialog node has an accessible name', + help: 'ARIA dialog and alertdialog nodes should have an accessible name', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-dialog-name?application=axeAPI', + tags: ['cat.aria', 'best-practice'], + }, + { + ruleId: 'aria-hidden-body', + description: + "Ensures aria-hidden='true' is not present on the document body.", + help: "aria-hidden='true' must not be present on the document body", + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-hidden-body?application=axeAPI', + tags: ['cat.aria', 'wcag2a', 'wcag412'], + }, + { + ruleId: 'aria-hidden-focus', + description: + 'Ensures aria-hidden elements are not focusable nor contain focusable elements', + help: 'ARIA hidden element must not be focusable or contain focusable elements', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-hidden-focus?application=axeAPI', + tags: ['cat.name-role-value', 'wcag2a', 'wcag412'], + actIds: ['6cfa84'], + }, + { + ruleId: 'aria-input-field-name', + description: 'Ensures every ARIA input field has an accessible name', + help: 'ARIA input fields must have an accessible name', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-input-field-name?application=axeAPI', + tags: ['cat.aria', 'wcag2a', 'wcag412', 'ACT'], + actIds: ['e086e5'], + }, + { + ruleId: 'aria-meter-name', + description: 'Ensures every ARIA meter node has an accessible name', + help: 'ARIA meter nodes must have an accessible name', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-meter-name?application=axeAPI', + tags: ['cat.aria', 'wcag2a', 'wcag111'], + }, + { + ruleId: 'aria-progressbar-name', + description: 'Ensures every ARIA progressbar node has an accessible name', + help: 'ARIA progressbar nodes must have an accessible name', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-progressbar-name?application=axeAPI', + tags: ['cat.aria', 'wcag2a', 'wcag111'], + }, + { + ruleId: 'aria-required-attr', + description: + 'Ensures elements with ARIA roles have all required ARIA attributes', + help: 'Required ARIA attributes must be provided', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-required-attr?application=axeAPI', + tags: ['cat.aria', 'wcag2a', 'wcag412'], + actIds: ['4e8ab6'], + }, + { + ruleId: 'aria-required-children', + description: + 'Ensures elements with an ARIA role that require child roles contain them', + help: 'Certain ARIA roles must contain particular children', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-required-children?application=axeAPI', + tags: ['cat.aria', 'wcag2a', 'wcag131'], + actIds: ['bc4a75', 'ff89c9'], + }, + { + ruleId: 'aria-required-parent', + description: + 'Ensures elements with an ARIA role that require parent roles are contained by them', + help: 'Certain ARIA roles must be contained by particular parents', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-required-parent?application=axeAPI', + tags: ['cat.aria', 'wcag2a', 'wcag131'], + actIds: ['ff89c9'], + }, + { + ruleId: 'aria-roledescription', + description: + 'Ensure aria-roledescription is only used on elements with an implicit or explicit role', + help: 'aria-roledescription must be on elements with a semantic role', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-roledescription?application=axeAPI', + tags: ['cat.aria', 'wcag2a', 'wcag412'], + }, + { + ruleId: 'aria-roles', + description: 'Ensures all elements with a role attribute use a valid value', + help: 'ARIA roles used must conform to valid values', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-roles?application=axeAPI', + tags: ['cat.aria', 'wcag2a', 'wcag412'], + actIds: ['674b10'], + }, + { + ruleId: 'aria-text', + description: + 'Ensures "role=text" is used on elements with no focusable descendants', + help: '"role=text" should have no focusable descendants', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-text?application=axeAPI', + tags: ['cat.aria', 'best-practice'], + }, + { + ruleId: 'aria-toggle-field-name', + description: 'Ensures every ARIA toggle field has an accessible name', + help: 'ARIA toggle fields must have an accessible name', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-toggle-field-name?application=axeAPI', + tags: ['cat.aria', 'wcag2a', 'wcag412', 'ACT'], + actIds: ['e086e5'], + }, + { + ruleId: 'aria-tooltip-name', + description: 'Ensures every ARIA tooltip node has an accessible name', + help: 'ARIA tooltip nodes must have an accessible name', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-tooltip-name?application=axeAPI', + tags: ['cat.aria', 'wcag2a', 'wcag412'], + }, + { + ruleId: 'aria-treeitem-name', + description: 'Ensures every ARIA treeitem node has an accessible name', + help: 'ARIA treeitem nodes should have an accessible name', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-treeitem-name?application=axeAPI', + tags: ['cat.aria', 'best-practice'], + }, + { + ruleId: 'aria-valid-attr-value', + description: 'Ensures all ARIA attributes have valid values', + help: 'ARIA attributes must conform to valid values', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-valid-attr-value?application=axeAPI', + tags: ['cat.aria', 'wcag2a', 'wcag412'], + actIds: ['6a7281'], + }, + { + ruleId: 'aria-valid-attr', + description: + 'Ensures attributes that begin with aria- are valid ARIA attributes', + help: 'ARIA attributes must conform to valid names', + helpUrl: + 'https://dequeuniversity.com/rules/axe/4.6/aria-valid-attr?application=axeAPI', + tags: ['cat.aria', 'wcag2a', 'wcag412'], + actIds: ['5f99a7'], + }, + { + ruleId: 'audio-caption', + description: 'Ensures