diff --git a/kayle/README.md b/kayle/README.md index f3cade2..bb7cd99 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) +- `kayle` (experimental): run tests using with [Rust Kayle_Innate or accessibility-rs](https://github.com/a11ywatch/accessibility-rs). - `custom`: custom runners using `injectRunner` util. ## Playwright/Puppeteer @@ -103,7 +104,8 @@ If you are using puppeteer expect around 2x slower results. Straight forward linting. You can pass a url or valid html. -Linting is handled on the same machine not sandboxed. +Linting is handled on the same machine not sandboxed. You can also use the `kayleInnateBuilder` to +get the wasm kayle setup to lint pages. ```js import { kayleLint } from "kayle/lint"; diff --git a/kayle/lib/common.ts b/kayle/lib/common.ts index 1bc344b..759687f 100644 --- a/kayle/lib/common.ts +++ b/kayle/lib/common.ts @@ -1,5 +1,7 @@ import { RunnerConfig } from "./config"; +export type IssueType = "error" | "warning" | "notice"; + export type MetaInfo = { errorCount: number; warningCount: number; @@ -11,17 +13,37 @@ export type Issue = { context: string; code: string; message: string; - type: "error" | "warning" | "notice"; + type: IssueType; typeCode: number; - runner: "htmlcs" | "axe" | "a11ywatch"; + // kayle is mapped from accessibility-rs + runner: "htmlcs" | "axe" | "kayle"; runnerExtras: Record; recurrence: number; selector: string; // the position on the dom to use for screenshots, targets, and etc. - clip?: DOMRect; + clip?: Pick; // base64 image to display in browser. clipBase64?: string; }; + +export type InnateIssue = { + context: string; + selectors: string[]; + code: string; + issue_type: IssueType; + type_code: number; + message: string; + runner: "accessibility-rs"; + runner_extras: { help_url: string; description: string; impact: string }; + recurrence: number; + clip?: { + x: number; + y: number; + height: number; + width: number; + }; +}; + // indexs of automatable issues export type Automatable = { // indexs of all missing alt tags. diff --git a/kayle/lib/config.ts b/kayle/lib/config.ts index 6b2bea9..54d1880 100644 --- a/kayle/lib/config.ts +++ b/kayle/lib/config.ts @@ -175,6 +175,10 @@ export type RunnerConfig = { _watcher?: Watcher; // initial fake request ran to enable Js _initRequest?: boolean; + // the incomplete kayle wasm runner + _kayleRunner?: boolean; + // contains a base runner htmlcs or axe + _includesBaseRunner?: boolean; }; // log singleton diff --git a/kayle/lib/kayle.ts b/kayle/lib/kayle.ts index d6cdad4..ba85456 100644 --- a/kayle/lib/kayle.ts +++ b/kayle/lib/kayle.ts @@ -4,7 +4,8 @@ import { RunnerConfig, _log } from "./config"; import { runnersJavascript, getRunner } from "./runner-js"; import { goToPage, setNetworkInterception } from "./utils/go-to-page"; import { Watcher } from "./watcher"; -import { Audit, RunnerConf } from "./common"; +import { Audit, type InnateIssue, RunnerConf } from "./common"; +import { getAllCss } from "./wasm"; // perform audit const audit = async (config: RunnerConfig): Promise => { @@ -98,6 +99,65 @@ export const auditExtension = async (config: RunnerConfig): Promise => { ); }; +// lazy load kayle innate +let kayle_innate; + +// run the rust wasm audit. +// we do not need timers here since almost all audits perform under 25ms. +// we still need to add the score map and apply the score based on what is found. +// the thing is we need the old map to make sure we do not repeat values. +const auditPageInnate = async ( + config: ReturnType, + results: Audit +) => { + if (!config._includesBaseRunner) { + await runActionsList(config as RunnerConfig); + } + + const html = await config.page.content(); + const css = await getAllCss(config as RunnerConfig); + + if (!kayle_innate) { + kayle_innate = await import("kayle_innate"); + } + + const innateAudit: InnateIssue[] = await kayle_innate.audit( + html, + css, + config.clip + ); + + for (const innateIssue of innateAudit) { + if (innateIssue.issue_type === "error") { + results.meta.errorCount += innateIssue.recurrence || 1; + } + if (innateIssue.issue_type === "warning") { + results.meta.warningCount += innateIssue.recurrence || 1; + } + if (innateIssue.issue_type === "notice") { + results.meta.noticeCount += innateIssue.recurrence || 1; + } + + results.issues.push({ + code: innateIssue.code, + type: innateIssue.issue_type, + typeCode: innateIssue.type_code, + message: innateIssue.message, + recurrence: innateIssue.recurrence, + clip: innateIssue.clip, + runner: "kayle", + runnerExtras: innateIssue.runner_extras, + selector: innateIssue.selectors.join(","), // combo the selectors + context: innateIssue.context, + }); + + // add value to missing alt index handling + if (innateIssue.code === "Principle1.Guideline1_1.1_1_1.H37") { + results.automateable.missingAltIndexs.push(results.issues.length - 1); + } + } +}; + /** * Run accessibility tests for page. * @param {Object} [config={}] config - Options to change the way tests run. @@ -125,11 +185,9 @@ export const kayle = async ( clearTimeout(watcher.timer as number); - if (results && o.clip && Array.isArray(results.issues)) { + if (results && o.clip && o.clip2Base64 && Array.isArray(results.issues)) { results.issues = await Promise.all( results.issues.map(async (item) => { - const { clip, selector } = item; - // prevent screenshots if (typeof o.clipMax === "number") { if (!o.clipMax) { @@ -143,15 +201,13 @@ export const kayle = async ( path: o.clipDir ? `${o.clipDir}${ o.clipDir.endsWith("/") ? "" : "/" - }${selector.trim()}.png` + }${item.selector.trim()}.png` : undefined, - clip, + clip: item.clip, }); // use a dynamic property to inject - todo: set the config initially before this iteration to keep shape aligned. - if (o.clip2Base64) { - item.clipBase64 = buffer.toString("base64"); - } + item.clipBase64 = buffer.toString("base64"); } catch (_) { // most likely not in the viewport // console.error(e); @@ -163,6 +219,10 @@ export const kayle = async ( ); } + if (config._kayleRunner) { + await auditPageInnate(config, results); + } + if (!preventClose && navigate) { try { await config.page.close(); diff --git a/kayle/lib/option.ts b/kayle/lib/option.ts index d2b5850..372f229 100644 --- a/kayle/lib/option.ts +++ b/kayle/lib/option.ts @@ -19,12 +19,15 @@ export function extractArgs(o, watcher?: Watcher) { includeWarnings: o.includeWarnings, rootElement: o.rootElement, rules: o.rules || [], + // soon move default to kayle runners: o.runners || ["htmlcs"], standard: o.standard, origin: o.origin || (o.html && "http://localhost") || "", language: o.language || "en", // store clip tracking element position clip: o.clip, + _kayleRunner: false, + _includesBaseRunner: false, }; // parse hidden elements into string @@ -45,16 +48,19 @@ export function extractArgs(o, watcher?: Watcher) { // default to a runner if ( - !options.runners.some( - (runner) => runner === "axe" || runner === "htmlcs" - // || - // // wasm build when released - // runner === "kayle" - ) + options.runners.forEach((runner, runnerIndex, ar) => { + if (runner === "axe" || runner === "htmlcs") { + options._includesBaseRunner = true; + } + if (runner === "kayle") { + options._kayleRunner = true; + ar.splice(runnerIndex, 1); + } + }) && + (options._includesBaseRunner || options._kayleRunner) ) { options.runners.push("htmlcs"); } - // todo: validate all options return options; } diff --git a/kayle/lib/runner-js.ts b/kayle/lib/runner-js.ts index 74d926a..50ba167 100644 --- a/kayle/lib/runner-js.ts +++ b/kayle/lib/runner-js.ts @@ -89,7 +89,6 @@ const getRunner = ( export type Runner = Exclude< keyof typeof runnersJavascript, - | "kayle" | "htmlcs_es" | "htmlcs_ja" | "htmlcs_fr" diff --git a/kayle/lib/runner.ts b/kayle/lib/runner.ts index 4c15839..90c8379 100644 --- a/kayle/lib/runner.ts +++ b/kayle/lib/runner.ts @@ -60,7 +60,11 @@ if (issue.element) { context = getElementContext(issue.element); selector = getElementSelector(issue.element); - if (cliped && typeof issue.element.getBoundingClientRect === "function") { + if ( + cliped && + !issue.bounds && + 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 e23159d..10c7f04 100644 --- a/kayle/package.json +++ b/kayle/package.json @@ -1,6 +1,6 @@ { "name": "kayle", - "version": "0.8.12", + "version": "0.8.13", "description": "Extremely fast and accurate accessibility engine built for any headless tool like playwright or puppeteer.", "main": "./build/index.js", "keywords": [ diff --git a/kayle/tests/basic.ts b/kayle/tests/basic.ts index f43e424..4fe65d5 100644 --- a/kayle/tests/basic.ts +++ b/kayle/tests/basic.ts @@ -14,7 +14,7 @@ import { performance } from "perf_hooks"; const { issues, pageUrl, documentTitle, meta, automateable } = await kayle({ page, browser, - runners: ["htmlcs", "axe"], + runners: ["htmlcs", "axe", "kayle"], includeWarnings: true, html: drakeMock, standard: Standard.WCAG2AA, diff --git a/kayle/tests/innate.ts b/kayle/tests/innate.ts index e7e30e9..962d10e 100644 --- a/kayle/tests/innate.ts +++ b/kayle/tests/innate.ts @@ -1,6 +1,5 @@ -import { audit } from "kayle_innate"; import puppeteer from "puppeteer"; -import { innateBuilder, kayle } from "kayle"; +import { kayle } from "kayle"; import { drakeMock } from "./mocks/html-mock"; import { performance } from "perf_hooks"; @@ -11,15 +10,7 @@ import { performance } from "perf_hooks"; if (process.env.LOG_ENABLED) { page.on("console", (msg) => console.log("PAGE LOG:", msg.text())); } - const { html, css } = await innateBuilder({ - page, - browser, - includeWarnings: true, - origin: "https://www.drake.com", - html: drakeMock, - }); - - const mock = html + const mock = drakeMock .replace( "Drake Industries | Custom, Durable, High-Quality Labels, Asset Tags and Custom Server Bezels", "" @@ -33,35 +24,18 @@ import { performance } from "perf_hooks"; ); const startTime = performance.now(); - await audit(mock, css, false); - const nextTime = performance.now() - startTime; - console.log("Rust/WASM TIME ", nextTime); - - const st = performance.now(); - await kayle({ + // run kayle with a real browser against the page. + const issues = await kayle({ page, browser, - runners: ["htmlcs"], + runners: ["kayle"], includeWarnings: true, origin: "https://www.drake.com", - html: drakeMock, - noIntercept: true, + html: mock, }); - const nt = performance.now() - st; - console.log("FAST_HTMLCS TIME", nt); - - const s = performance.now(); - await kayle({ - page, - browser, - runners: ["axe"], - includeWarnings: true, - origin: "https://www.drake.com", - html: drakeMock, - noIntercept: true, - }); - const n = performance.now() - s; - console.log("FAST_AXE TIME", n); + const nextTime = performance.now() - startTime; + console.log("Kayle Innate TIME ", nextTime); + console.log(issues); await browser.close(); })();