@diegovelasquezweb/a11y-engine 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -2
- package/scripts/index.d.mts +62 -0
- package/scripts/index.mjs +182 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegovelasquezweb/a11y-engine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "WCAG 2.2 AA accessibility audit engine — scanner, analyzer, and report builders",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,9 +13,13 @@
|
|
|
13
13
|
"node": ">=18"
|
|
14
14
|
},
|
|
15
15
|
"exports": {
|
|
16
|
-
".":
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./scripts/index.d.mts",
|
|
18
|
+
"default": "./scripts/index.mjs"
|
|
19
|
+
}
|
|
17
20
|
},
|
|
18
21
|
"main": "scripts/index.mjs",
|
|
22
|
+
"types": "scripts/index.d.mts",
|
|
19
23
|
"bin": {
|
|
20
24
|
"a11y-audit": "scripts/audit.mjs"
|
|
21
25
|
},
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface SeverityTotals {
|
|
2
|
+
Critical: number;
|
|
3
|
+
Serious: number;
|
|
4
|
+
Moderate: number;
|
|
5
|
+
Minor: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ScoreResult {
|
|
9
|
+
score: number;
|
|
10
|
+
label: string;
|
|
11
|
+
wcagStatus: "Pass" | "Conditional Pass" | "Fail";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PersonaGroup {
|
|
15
|
+
label: string;
|
|
16
|
+
count: number;
|
|
17
|
+
icon: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface EngineAssets {
|
|
21
|
+
intelligence: Record<string, unknown>;
|
|
22
|
+
pa11yConfig: Record<string, unknown>;
|
|
23
|
+
complianceConfig: Record<string, unknown>;
|
|
24
|
+
wcagReference: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getAssets(): EngineAssets;
|
|
28
|
+
|
|
29
|
+
export function mapPa11yRuleToCanonical(
|
|
30
|
+
ruleId: string,
|
|
31
|
+
sourceRuleId?: string | null,
|
|
32
|
+
checkData?: Record<string, unknown> | null
|
|
33
|
+
): string;
|
|
34
|
+
|
|
35
|
+
export function enrichFindings<T extends Record<string, unknown>>(
|
|
36
|
+
findings: T[]
|
|
37
|
+
): T[];
|
|
38
|
+
|
|
39
|
+
export function computeScore(totals: SeverityTotals): ScoreResult;
|
|
40
|
+
|
|
41
|
+
export function computePersonaGroups(
|
|
42
|
+
findings: Record<string, unknown>[]
|
|
43
|
+
): Record<string, PersonaGroup>;
|
|
44
|
+
|
|
45
|
+
export interface ScanPayload {
|
|
46
|
+
findings: Record<string, unknown>[];
|
|
47
|
+
metadata?: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ReportOptions {
|
|
51
|
+
baseUrl?: string;
|
|
52
|
+
target?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function generatePDF(
|
|
56
|
+
payload: ScanPayload,
|
|
57
|
+
options?: ReportOptions
|
|
58
|
+
): Promise<Buffer>;
|
|
59
|
+
|
|
60
|
+
export function generateChecklist(
|
|
61
|
+
options?: Pick<ReportOptions, "baseUrl">
|
|
62
|
+
): Promise<string>;
|
package/scripts/index.mjs
CHANGED
|
@@ -260,3 +260,185 @@ export function computePersonaGroups(findings) {
|
|
|
260
260
|
|
|
261
261
|
return groups;
|
|
262
262
|
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Report generation
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
import {
|
|
269
|
+
normalizeFindings as normalizeForReports,
|
|
270
|
+
buildSummary,
|
|
271
|
+
computeComplianceScore,
|
|
272
|
+
scoreLabel,
|
|
273
|
+
buildPersonaSummary,
|
|
274
|
+
wcagOverallStatus,
|
|
275
|
+
} from "./reports/renderers/findings.mjs";
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Generates a PDF report buffer from raw scan findings.
|
|
279
|
+
* Requires Playwright (chromium) to render the PDF.
|
|
280
|
+
* @param {{ findings: object[], metadata?: object }} payload - Raw scan output (snake_case keys).
|
|
281
|
+
* @param {{ baseUrl?: string, target?: string }} [options={}]
|
|
282
|
+
* @returns {Promise<Buffer>} The PDF as a Node.js Buffer.
|
|
283
|
+
*/
|
|
284
|
+
export async function generatePDF(payload, options = {}) {
|
|
285
|
+
const { chromium } = await import("playwright");
|
|
286
|
+
const {
|
|
287
|
+
buildPdfCoverPage,
|
|
288
|
+
buildPdfTableOfContents,
|
|
289
|
+
buildPdfExecutiveSummary,
|
|
290
|
+
buildPdfRiskSection,
|
|
291
|
+
buildPdfRemediationRoadmap,
|
|
292
|
+
buildPdfMethodologySection,
|
|
293
|
+
buildPdfIssueSummaryTable,
|
|
294
|
+
buildPdfNextSteps,
|
|
295
|
+
buildPdfAuditLimitations,
|
|
296
|
+
} = await import("./reports/renderers/pdf.mjs");
|
|
297
|
+
|
|
298
|
+
const args = { baseUrl: options.baseUrl || "", target: options.target || "WCAG 2.2 AA" };
|
|
299
|
+
const findings = normalizeForReports(payload).filter(
|
|
300
|
+
(f) => f.wcagClassification !== "AAA" && f.wcagClassification !== "Best Practice",
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const totals = buildSummary(findings);
|
|
304
|
+
const score = computeComplianceScore(totals);
|
|
305
|
+
let siteHostname = args.baseUrl;
|
|
306
|
+
try { siteHostname = new URL(args.baseUrl.startsWith("http") ? args.baseUrl : `https://${args.baseUrl}`).hostname; } catch {}
|
|
307
|
+
const coverDate = new Date().toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
|
|
308
|
+
|
|
309
|
+
const html = `<!doctype html>
|
|
310
|
+
<html lang="en"><head><meta charset="utf-8"><title>Accessibility Audit — ${siteHostname}</title>
|
|
311
|
+
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
312
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
|
|
313
|
+
<style>@page{size:A4;margin:2cm}body{background:white;color:black;font-family:'Libre Baskerville',serif;font-size:11pt;line-height:1.6;margin:0;padding:0}h1,h2,h3,h4{font-family:'Inter',sans-serif;color:black;margin-top:1.5rem;margin-bottom:1rem}.cover-page{height:25.5cm;display:flex;flex-direction:column;page-break-after:always}.finding-entry{border-top:1pt solid black;padding-top:1.5rem;margin-top:2rem;page-break-inside:avoid}.severity-tag{font-weight:800;text-transform:uppercase;border:1.5pt solid black;padding:2pt 6pt;font-size:9pt;margin-bottom:1rem;display:inline-block}.remediation-box{background-color:#f3f4f6;border-left:4pt solid black;padding:1rem;margin:1rem 0;font-style:italic}pre{background:#f9fafb;border:1pt solid #ddd;padding:10pt;font-size:8pt;overflow:hidden;white-space:pre-wrap}.stats-table{width:100%;border-collapse:collapse;margin:2rem 0}.stats-table th,.stats-table td{border:1pt solid black;padding:10pt;text-align:left;font-size:9pt;font-family:'Inter',sans-serif}</style>
|
|
314
|
+
</head><body>
|
|
315
|
+
${buildPdfCoverPage({ siteHostname, target: args.target, score, wcagStatus: wcagOverallStatus(totals), coverDate })}
|
|
316
|
+
${buildPdfTableOfContents()}
|
|
317
|
+
${buildPdfExecutiveSummary(args, findings, totals)}
|
|
318
|
+
${buildPdfRiskSection(totals)}
|
|
319
|
+
${buildPdfRemediationRoadmap(findings)}
|
|
320
|
+
${buildPdfMethodologySection(args, findings)}
|
|
321
|
+
${buildPdfIssueSummaryTable(findings)}
|
|
322
|
+
${buildPdfNextSteps(findings, totals)}
|
|
323
|
+
${buildPdfAuditLimitations()}
|
|
324
|
+
</body></html>`;
|
|
325
|
+
|
|
326
|
+
const browser = await chromium.launch();
|
|
327
|
+
try {
|
|
328
|
+
const page = await browser.newPage();
|
|
329
|
+
await page.setContent(html, { waitUntil: "load" });
|
|
330
|
+
await page.evaluate(() => document.fonts.ready);
|
|
331
|
+
const pdfBuffer = await page.pdf({
|
|
332
|
+
format: "A4",
|
|
333
|
+
printBackground: true,
|
|
334
|
+
margin: { top: "1cm", right: "1cm", bottom: "1cm", left: "1cm" },
|
|
335
|
+
displayHeaderFooter: false,
|
|
336
|
+
});
|
|
337
|
+
return Buffer.from(pdfBuffer);
|
|
338
|
+
} finally {
|
|
339
|
+
await browser.close();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Generates a standalone manual accessibility checklist HTML string.
|
|
345
|
+
* Does not depend on scan results — reads from manual-checks.json asset.
|
|
346
|
+
* @param {{ baseUrl?: string }} [options={}]
|
|
347
|
+
* @returns {Promise<string>} The complete checklist HTML document.
|
|
348
|
+
*/
|
|
349
|
+
export async function generateChecklist(options = {}) {
|
|
350
|
+
const { buildManualCheckCard } = await import("./reports/renderers/html.mjs");
|
|
351
|
+
const { escapeHtml } = await import("./reports/renderers/utils.mjs");
|
|
352
|
+
|
|
353
|
+
const manualChecks = loadAssetJson(ASSET_PATHS.reporting.manualChecks, "manual-checks.json");
|
|
354
|
+
const siteLabel = options.baseUrl || "your site";
|
|
355
|
+
const cards = manualChecks.map((c) => buildManualCheckCard(c)).join("\n");
|
|
356
|
+
|
|
357
|
+
const TOTAL = manualChecks.length;
|
|
358
|
+
const COUNT_A = manualChecks.filter((c) => c.level === "A").length;
|
|
359
|
+
const COUNT_AA = manualChecks.filter((c) => c.level === "AA").length;
|
|
360
|
+
const COUNT_AT = manualChecks.filter((c) => c.level === "AT").length;
|
|
361
|
+
|
|
362
|
+
const selectClasses =
|
|
363
|
+
"pl-4 pr-10 py-3 bg-white border border-slate-300 rounded-2xl text-sm font-bold text-slate-800 focus:outline-none focus:ring-4 focus:ring-amber-500/20 focus:border-amber-400 shadow-sm transition-all appearance-none cursor-pointer bg-[url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20stroke%3D%22%23374151%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%221.5%22%3E%3Cpath%20d%3D%22m6%208%204%204%204-4%22%2F%3E%3C%2Fsvg%3E')] bg-[length:1.25rem_1.25rem] bg-[right_0.5rem_center] bg-no-repeat";
|
|
364
|
+
|
|
365
|
+
// Import the full checklist builder to reuse its buildHtml
|
|
366
|
+
// The checklist builder module has a main() that auto-runs, so we dynamically
|
|
367
|
+
// construct the same output using the renderer functions directly.
|
|
368
|
+
return `<!doctype html>
|
|
369
|
+
<html lang="en">
|
|
370
|
+
<head>
|
|
371
|
+
<meta charset="utf-8">
|
|
372
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
373
|
+
<title>Manual Accessibility Checklist — ${escapeHtml(siteLabel)}</title>
|
|
374
|
+
<script src="https://cdn.tailwindcss.com"><\/script>
|
|
375
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
376
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
377
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
378
|
+
<style>
|
|
379
|
+
:root { --amber: hsl(38, 92%, 50%); }
|
|
380
|
+
html { scroll-padding-top: 80px; }
|
|
381
|
+
body { background-color: #f8fafc; font-family: 'Inter', sans-serif; -webkit-font-smoothing: antialiased; }
|
|
382
|
+
.glass-header { background: rgba(255,255,255,0.85); backdrop-filter: blur(12px) saturate(180%); }
|
|
383
|
+
</style>
|
|
384
|
+
</head>
|
|
385
|
+
<body class="text-slate-900 min-h-screen">
|
|
386
|
+
<header class="fixed top-0 left-0 right-0 z-50 glass-header border-b border-slate-200/80 shadow-sm" id="navbar">
|
|
387
|
+
<nav aria-label="Checklist header">
|
|
388
|
+
<div class="max-w-4xl mx-auto px-4 h-16 flex justify-between items-center">
|
|
389
|
+
<div class="flex items-center gap-3">
|
|
390
|
+
<div class="px-3 h-10 rounded-lg bg-slate-900 text-white font-bold text-base font-mono flex items-center justify-center shadow-md">a11y</div>
|
|
391
|
+
<h1 class="text-xl font-bold">Manual <span class="text-slate-500">Checklist</span></h1>
|
|
392
|
+
</div>
|
|
393
|
+
<span class="text-sm text-slate-500 font-medium">${escapeHtml(siteLabel)}</span>
|
|
394
|
+
</div>
|
|
395
|
+
</nav>
|
|
396
|
+
</header>
|
|
397
|
+
<main id="main-content" class="max-w-4xl mx-auto px-4 pt-24 pb-20">
|
|
398
|
+
<div class="mb-12">
|
|
399
|
+
<h2 class="text-3xl font-extrabold mb-2">Manual Testing Checklist</h2>
|
|
400
|
+
<p class="text-slate-600 text-base leading-relaxed max-w-2xl">Automated scans catch ~30-40% of accessibility issues. This checklist covers the rest: keyboard navigation, screen reader behaviour, cognitive flow and more.</p>
|
|
401
|
+
</div>
|
|
402
|
+
<div class="flex flex-wrap items-center gap-3 mb-8">
|
|
403
|
+
<div class="flex items-center gap-1.5 text-sm font-semibold"><span class="inline-block w-3 h-3 rounded-full bg-slate-300"></span><span id="count-total">${TOTAL}</span> Total</div>
|
|
404
|
+
<div class="flex items-center gap-1.5 text-sm font-semibold text-emerald-600"><span class="inline-block w-3 h-3 rounded-full bg-emerald-400"></span><span id="count-pass">0</span> Pass</div>
|
|
405
|
+
<div class="flex items-center gap-1.5 text-sm font-semibold text-rose-600"><span class="inline-block w-3 h-3 rounded-full bg-rose-400"></span><span id="count-fail">0</span> Fail</div>
|
|
406
|
+
<div class="flex items-center gap-1.5 text-sm font-semibold text-amber-600"><span class="inline-block w-3 h-3 rounded-full bg-amber-400"></span><span id="count-na">0</span> N/A</div>
|
|
407
|
+
<div class="ml-auto flex items-center gap-2">
|
|
408
|
+
<select id="level-filter" class="${selectClasses}">
|
|
409
|
+
<option value="all">All levels</option>
|
|
410
|
+
<option value="A">Level A (${COUNT_A})</option>
|
|
411
|
+
<option value="AA">Level AA (${COUNT_AA})</option>
|
|
412
|
+
<option value="AT">Assistive Tech (${COUNT_AT})</option>
|
|
413
|
+
</select>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
<div id="checklist-items" class="space-y-4">${cards}</div>
|
|
417
|
+
</main>
|
|
418
|
+
<script>
|
|
419
|
+
const items = document.querySelectorAll('[data-check]');
|
|
420
|
+
function updateProgress() {
|
|
421
|
+
let pass=0,fail=0,na=0;
|
|
422
|
+
items.forEach(el => { const s=el.dataset.status; if(s==='pass')pass++; else if(s==='fail')fail++; else if(s==='na')na++; });
|
|
423
|
+
document.getElementById('count-pass').textContent=pass;
|
|
424
|
+
document.getElementById('count-fail').textContent=fail;
|
|
425
|
+
document.getElementById('count-na').textContent=na;
|
|
426
|
+
}
|
|
427
|
+
document.getElementById('level-filter').addEventListener('change',e=>{
|
|
428
|
+
const v=e.target.value;
|
|
429
|
+
items.forEach(el=>{el.style.display=(v==='all'||el.dataset.level===v)?'':'none';});
|
|
430
|
+
});
|
|
431
|
+
items.forEach(el=>{
|
|
432
|
+
el.querySelectorAll('[data-action]').forEach(btn=>{
|
|
433
|
+
btn.addEventListener('click',()=>{
|
|
434
|
+
const action=btn.dataset.action;
|
|
435
|
+
el.dataset.status=el.dataset.status===action?'none':action;
|
|
436
|
+
updateProgress();
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
updateProgress();
|
|
441
|
+
<\/script>
|
|
442
|
+
</body>
|
|
443
|
+
</html>`;
|
|
444
|
+
}
|