📦 EqualifyEverything / equalify-iris

📄 lint.ts · 58 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58import { JSDOM, VirtualConsole } from "jsdom";
import axe from "axe-core";

export interface LintViolation {
  id: string;
  impact: string | null;
  description: string;
  nodes: number;
}

export interface LintResult {
  ok: boolean;
  violations: LintViolation[];
  error?: string;
}

// PRD §7.7: validate the document parses and basic accessibility lint passes
// (axe-core in headless mode). We run axe inside a jsdom realm. If axe cannot
// run in this environment we degrade to a parse check rather than fail the run;
// either way the result is surfaced to the Reader as input.
export async function runAxe(html: string): Promise<LintResult> {
  let dom: JSDOM;
  try {
    // Swallow jsdom's not-implemented noise (e.g. canvas getContext, which the
    // disabled color-contrast rule would otherwise trigger).
    const virtualConsole = new VirtualConsole();
    dom = new JSDOM(html, { runScripts: "outside-only", pretendToBeVisual: true, virtualConsole });
  } catch (e) {
    return { ok: false, violations: [], error: `document failed to parse: ${(e as Error).message}` };
  }

  try {
    const { window } = dom;
    // Inject the axe-core library source into the jsdom realm and run it there.
    window.eval(axe.source);
    const w = window as unknown as {
      axe: { run: (ctx: unknown, opts: unknown) => Promise<{ violations: { id: string; impact: string | null; description: string; nodes: unknown[] }[] }> };
    };
    const results = await w.axe.run(window.document, {
      runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"] },
      // Output is content-only with no styling (PRD §4), so color contrast is
      // out of scope and cannot be assessed without rendering anyway.
      rules: { "color-contrast": { enabled: false } },
    });
    const violations = results.violations.map((v) => ({
      id: v.id,
      impact: v.impact,
      description: v.description,
      nodes: v.nodes.length,
    }));
    return { ok: violations.length === 0, violations };
  } catch (e) {
    return { ok: true, violations: [], error: `axe-core could not run in this environment: ${(e as Error).message}` };
  } finally {
    dom.window.close();
  }
}