@diegovelasquezweb/a11y-engine 0.1.0
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/LICENSE +21 -0
- package/README.md +20 -0
- package/assets/discovery/crawler-config.json +11 -0
- package/assets/discovery/stack-detection.json +33 -0
- package/assets/remediation/axe-check-maps.json +31 -0
- package/assets/remediation/code-patterns.json +109 -0
- package/assets/remediation/guardrails.json +24 -0
- package/assets/remediation/intelligence.json +4166 -0
- package/assets/remediation/source-boundaries.json +46 -0
- package/assets/reporting/compliance-config.json +173 -0
- package/assets/reporting/manual-checks.json +944 -0
- package/assets/reporting/wcag-reference.json +588 -0
- package/package.json +37 -0
- package/scripts/audit.mjs +326 -0
- package/scripts/core/asset-loader.mjs +54 -0
- package/scripts/core/toolchain.mjs +102 -0
- package/scripts/core/utils.mjs +105 -0
- package/scripts/engine/analyzer.mjs +1022 -0
- package/scripts/engine/dom-scanner.mjs +685 -0
- package/scripts/engine/source-scanner.mjs +300 -0
- package/scripts/reports/builders/checklist.mjs +307 -0
- package/scripts/reports/builders/html.mjs +766 -0
- package/scripts/reports/builders/md.mjs +96 -0
- package/scripts/reports/builders/pdf.mjs +259 -0
- package/scripts/reports/renderers/findings.mjs +188 -0
- package/scripts/reports/renderers/html.mjs +452 -0
- package/scripts/reports/renderers/md.mjs +595 -0
- package/scripts/reports/renderers/pdf.mjs +551 -0
- package/scripts/reports/renderers/utils.mjs +42 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file format-pdf.mjs
|
|
3
|
+
* @description PDF report component builders and formatting logic.
|
|
4
|
+
* Generates the structural HTML parts for the executive summary, legal risk,
|
|
5
|
+
* remediation roadmap, and technical methodology for the PDF report.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ASSET_PATHS, loadAssetJson } from "../../core/asset-loader.mjs";
|
|
9
|
+
import { computeComplianceScore, scoreLabel, wcagOverallStatus } from "./findings.mjs";
|
|
10
|
+
import { escapeHtml } from "./utils.mjs";
|
|
11
|
+
|
|
12
|
+
const COMPLIANCE_CONFIG = loadAssetJson(
|
|
13
|
+
ASSET_PATHS.reporting.complianceConfig,
|
|
14
|
+
"assets/reporting/compliance-config.json",
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Maps compliance performance labels to risk assessments for the executive summary.
|
|
19
|
+
* @type {Object<string, string>}
|
|
20
|
+
*/
|
|
21
|
+
const RISK_LABELS = COMPLIANCE_CONFIG.riskLabels;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns the risk metrics (label and risk assessment) for a given compliance score.
|
|
25
|
+
* @param {number} score - The calculated compliance score.
|
|
26
|
+
* @returns {Object} An object containing 'label' and 'risk' strings.
|
|
27
|
+
*/
|
|
28
|
+
export function scoreMetrics(score) {
|
|
29
|
+
const label = scoreLabel(score);
|
|
30
|
+
return { label, risk: RISK_LABELS[label] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Builds the Table of Contents page for the PDF report.
|
|
35
|
+
* @returns {string} The HTML string for the ToC page.
|
|
36
|
+
*/
|
|
37
|
+
export function buildPdfTableOfContents() {
|
|
38
|
+
const sections = [
|
|
39
|
+
["1.", "Executive Summary"],
|
|
40
|
+
["2.", "Compliance & Legal Risk"],
|
|
41
|
+
["3.", "Remediation Roadmap"],
|
|
42
|
+
["4.", "Methodology & Scope"],
|
|
43
|
+
["5.", "Issue Summary"],
|
|
44
|
+
["6.", "Recommended Next Steps"],
|
|
45
|
+
["7.", "Audit Scope & Limitations"],
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const rows = sections
|
|
49
|
+
.map(
|
|
50
|
+
([num, title]) => `
|
|
51
|
+
<tr>
|
|
52
|
+
<td style="width: 2.5cm; font-family: 'Inter', sans-serif; font-weight: 700; font-size: 10pt; padding: 8pt 10pt; border: none; border-bottom: 1pt solid #f3f4f6; color: #6b7280;">${num}</td>
|
|
53
|
+
<td style="font-family: 'Inter', sans-serif; font-size: 10pt; padding: 8pt 10pt; border: none; border-bottom: 1pt solid #f3f4f6;">${title}</td>
|
|
54
|
+
</tr>`,
|
|
55
|
+
)
|
|
56
|
+
.join("");
|
|
57
|
+
|
|
58
|
+
return `
|
|
59
|
+
<div style="page-break-before: always;">
|
|
60
|
+
<p style="font-family: 'Inter', sans-serif; font-size: 7pt; font-weight: 700; letter-spacing: 3.5pt; text-transform: uppercase; color: #9ca3af; margin: 0 0 1.5rem 0;">Contents</p>
|
|
61
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
62
|
+
<tbody>${rows}</tbody>
|
|
63
|
+
</table>
|
|
64
|
+
</div>`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Builds the Executive Summary section for the PDF report.
|
|
69
|
+
* @param {Object} args - The parsed CLI arguments.
|
|
70
|
+
* @param {Object[]} findings - The list of normalized findings.
|
|
71
|
+
* @param {Object<string, number>} totals - Summary counts per severity.
|
|
72
|
+
* @returns {string} The HTML string for the executive summary section.
|
|
73
|
+
*/
|
|
74
|
+
export function buildPdfExecutiveSummary(args, findings, totals) {
|
|
75
|
+
const blockers = findings.filter(
|
|
76
|
+
(f) => f.severity === "Critical" || f.severity === "Serious",
|
|
77
|
+
);
|
|
78
|
+
const totalIssues = findings.length;
|
|
79
|
+
const pagesAffected = new Set(findings.map((f) => f.area)).size;
|
|
80
|
+
|
|
81
|
+
const topIssues = blockers
|
|
82
|
+
.slice(0, 3)
|
|
83
|
+
.map(
|
|
84
|
+
(f) =>
|
|
85
|
+
`<li style="margin-bottom: 6pt;">${escapeHtml(f.title)} — <em>${escapeHtml(f.area)}</em></li>`,
|
|
86
|
+
)
|
|
87
|
+
.join("");
|
|
88
|
+
|
|
89
|
+
const narrative =
|
|
90
|
+
totalIssues === 0
|
|
91
|
+
? `<p style="line-height: 1.8; font-size: 10pt; margin-bottom: 8pt;">
|
|
92
|
+
The automated scan of <strong>${escapeHtml(args.baseUrl)}</strong> detected no WCAG 2.2 AA violations across
|
|
93
|
+
the scanned routes. This is a strong result. Six criteria require manual verification before full
|
|
94
|
+
compliance can be certified — see Section 6.
|
|
95
|
+
</p>`
|
|
96
|
+
: `<p style="line-height: 1.8; font-size: 10pt; margin-bottom: 8pt;">
|
|
97
|
+
The automated scan of <strong>${escapeHtml(args.baseUrl)}</strong> identified <strong>${totalIssues} accessibility
|
|
98
|
+
violation${totalIssues !== 1 ? "s" : ""}</strong> across <strong>${pagesAffected} page${pagesAffected !== 1 ? "s" : ""}</strong>,
|
|
99
|
+
including <strong>${totals.Critical} Critical</strong> and <strong>${totals.Serious} Serious</strong> severity issues
|
|
100
|
+
that constitute immediate barriers for users relying on assistive technology.
|
|
101
|
+
</p>
|
|
102
|
+
<p style="line-height: 1.8; font-size: 10pt; margin-bottom: 8pt;">
|
|
103
|
+
Critical issues prevent disabled users from completing core tasks entirely.
|
|
104
|
+
Serious issues create significant friction that forces users to abandon flows.
|
|
105
|
+
Together, these ${totals.Critical + totals.Serious} blockers represent the primary compliance and
|
|
106
|
+
user experience risk for the organization.
|
|
107
|
+
</p>`;
|
|
108
|
+
|
|
109
|
+
const topIssuesBlock =
|
|
110
|
+
blockers.length > 0
|
|
111
|
+
? `
|
|
112
|
+
<p style="font-family: sans-serif; font-size: 9pt; font-weight: 800; text-transform: uppercase; letter-spacing: 1pt; margin: 1.2rem 0 6pt 0; color: #6b7280;">Priority Issues</p>
|
|
113
|
+
<ul style="margin: 0; padding-left: 1.2rem; font-size: 10pt;">${topIssues}</ul>`
|
|
114
|
+
: "";
|
|
115
|
+
|
|
116
|
+
const conformanceStatement =
|
|
117
|
+
totals.Critical > 0
|
|
118
|
+
? `<strong>Does not conform</strong> to ${escapeHtml(args.target)}`
|
|
119
|
+
: findings.length > 0
|
|
120
|
+
? `<strong>Partially conforms</strong> to ${escapeHtml(args.target)}`
|
|
121
|
+
: `<strong>Conforms</strong> to ${escapeHtml(args.target)} (automated checks)`;
|
|
122
|
+
|
|
123
|
+
return `
|
|
124
|
+
<div style="page-break-before: always;">
|
|
125
|
+
<h2 style="margin-top: 0;">1. Executive Summary</h2>
|
|
126
|
+
|
|
127
|
+
<p style="font-size: 10pt; line-height: 1.7; margin-bottom: 1rem; padding: 0.8rem 1rem; border: 1pt solid #e5e7eb; background: #f9fafb;">
|
|
128
|
+
<strong>Conformance status:</strong> This site ${conformanceStatement} based on automated testing conducted on ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}.
|
|
129
|
+
See Section 6 for scope and limitations.
|
|
130
|
+
</p>
|
|
131
|
+
|
|
132
|
+
${narrative}
|
|
133
|
+
${topIssuesBlock}
|
|
134
|
+
|
|
135
|
+
<table class="stats-table" style="margin-top: 1.5rem;">
|
|
136
|
+
<thead>
|
|
137
|
+
<tr><th>Severity</th><th>Count</th><th>Impact</th><th>Action Required</th></tr>
|
|
138
|
+
</thead>
|
|
139
|
+
<tbody>
|
|
140
|
+
${COMPLIANCE_CONFIG.severityDefinitions.map((d) => `<tr><td><strong>${escapeHtml(d.level)}</strong></td><td>${totals[d.level]}</td><td>${escapeHtml(d.impact)}</td><td>${escapeHtml(d.action)}</td></tr>`).join("\n ")}
|
|
141
|
+
</tbody>
|
|
142
|
+
</table>
|
|
143
|
+
</div>`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Builds the Compliance & Legal Risk section for the PDF report.
|
|
148
|
+
* @param {Object<string, number>} totals - Summary counts per severity.
|
|
149
|
+
* @returns {string} The HTML string for the risk analysis section.
|
|
150
|
+
*/
|
|
151
|
+
export function buildPdfRiskSection(totals) {
|
|
152
|
+
const score = computeComplianceScore(totals);
|
|
153
|
+
const level =
|
|
154
|
+
COMPLIANCE_CONFIG.riskLevels.find((l) => score >= l.minScore) ||
|
|
155
|
+
COMPLIANCE_CONFIG.riskLevels[COMPLIANCE_CONFIG.riskLevels.length - 1];
|
|
156
|
+
const riskLevel = level.label;
|
|
157
|
+
const riskColor = level.color;
|
|
158
|
+
|
|
159
|
+
const regulationRows = COMPLIANCE_CONFIG.regulations
|
|
160
|
+
.map(
|
|
161
|
+
(reg) =>
|
|
162
|
+
`<tr>
|
|
163
|
+
<td><strong>${escapeHtml(reg.name)}</strong></td>
|
|
164
|
+
<td>${escapeHtml(reg.jurisdiction)}</td>
|
|
165
|
+
<td>${escapeHtml(reg.standard)}</td>
|
|
166
|
+
<td>${escapeHtml(reg.deadline)}</td>
|
|
167
|
+
</tr>`,
|
|
168
|
+
)
|
|
169
|
+
.join("\n ");
|
|
170
|
+
|
|
171
|
+
return `
|
|
172
|
+
<div style="page-break-before: always;">
|
|
173
|
+
<h2 style="margin-top: 0;">2. Compliance & Legal Risk</h2>
|
|
174
|
+
<p style="line-height: 1.8; font-size: 10pt; margin-bottom: 1rem;">
|
|
175
|
+
Web accessibility compliance is governed by international standards and increasingly enforced
|
|
176
|
+
by law across major markets. The following regulations apply to most digital products and services.
|
|
177
|
+
</p>
|
|
178
|
+
|
|
179
|
+
<table class="stats-table">
|
|
180
|
+
<thead>
|
|
181
|
+
<tr><th>Regulation</th><th>Jurisdiction</th><th>Standard</th><th>Deadline</th></tr>
|
|
182
|
+
</thead>
|
|
183
|
+
<tbody>
|
|
184
|
+
${regulationRows}
|
|
185
|
+
</tbody>
|
|
186
|
+
</table>
|
|
187
|
+
|
|
188
|
+
${(() => {
|
|
189
|
+
const inForce = COMPLIANCE_CONFIG.regulations
|
|
190
|
+
.filter((r) => r.deadline.toLowerCase().includes("in force"))
|
|
191
|
+
.map((r) => r.name)
|
|
192
|
+
.slice(0, 3)
|
|
193
|
+
.join(", ");
|
|
194
|
+
const upcoming = COMPLIANCE_CONFIG.regulations
|
|
195
|
+
.filter((r) => !r.deadline.toLowerCase().includes("in force"))
|
|
196
|
+
.map((r) => `${r.name} (${r.deadline})`)
|
|
197
|
+
.slice(0, 2)
|
|
198
|
+
.join(", ");
|
|
199
|
+
const wcagStatus = wcagOverallStatus(totals);
|
|
200
|
+
const riskText =
|
|
201
|
+
wcagStatus === "Fail"
|
|
202
|
+
? `The site has unresolved Critical or Serious issues that prevent WCAG conformance. Immediate remediation is required. Applicable in-force regulations: ${inForce}${upcoming ? `. Upcoming deadlines: ${upcoming}` : ""}.`
|
|
203
|
+
: score >= 75
|
|
204
|
+
? `The site demonstrates strong accessibility fundamentals. Remaining issues should be addressed to achieve full compliance before applicable regulatory deadlines. In-force regulations include: ${inForce}.`
|
|
205
|
+
: score >= 55
|
|
206
|
+
? `The site has meaningful accessibility gaps that create legal exposure. A remediation plan should be established and executed promptly. Applicable regulations include ${inForce}${upcoming ? `, with upcoming deadlines under ${upcoming}` : ""}.`
|
|
207
|
+
: `The site has significant accessibility barriers that create substantial legal exposure. Immediate remediation of Critical and Serious issues is strongly recommended. Applicable in-force regulations: ${inForce}${upcoming ? `. Upcoming deadlines: ${upcoming}` : ""}.`;
|
|
208
|
+
return `<div style="margin-top: 1.5rem; padding: 1rem 1.2rem; border: 1.5pt solid ${riskColor}; border-left: 5pt solid ${riskColor}; background: #f9fafb; page-break-inside: avoid; page-break-before: avoid;">
|
|
209
|
+
<p style="font-family: sans-serif; font-size: 9pt; font-weight: 800; text-transform: uppercase; letter-spacing: 1pt; margin: 0 0 4pt 0; color: #6b7280;">Current Risk Assessment</p>
|
|
210
|
+
<p style="font-family: sans-serif; font-size: 16pt; font-weight: 900; margin: 0; color: ${riskColor};">${riskLevel} Risk</p>
|
|
211
|
+
<p style="font-size: 9pt; margin: 6pt 0 0 0; color: #374151; line-height: 1.6;">${riskText}</p>
|
|
212
|
+
</div>`;
|
|
213
|
+
})()}
|
|
214
|
+
</div>`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Builds the Remediation Roadmap section for the PDF report.
|
|
219
|
+
* @param {Object[]} findings - The normalized findings to prioritize.
|
|
220
|
+
* @returns {string} The HTML string for the roadmap section.
|
|
221
|
+
*/
|
|
222
|
+
export function buildPdfRemediationRoadmap(findings) {
|
|
223
|
+
const critical = findings.filter((f) => f.severity === "Critical");
|
|
224
|
+
const serious = findings.filter((f) => f.severity === "Serious");
|
|
225
|
+
const moderate = findings.filter((f) => f.severity === "Moderate");
|
|
226
|
+
const minor = findings.filter((f) => f.severity === "Minor");
|
|
227
|
+
|
|
228
|
+
const mult = COMPLIANCE_CONFIG.effortMultipliers;
|
|
229
|
+
const effortHours = (c, s, mo, mi) =>
|
|
230
|
+
Math.round(c * mult.Critical + s * mult.Serious + mo * mult.Moderate + mi * mult.Minor);
|
|
231
|
+
|
|
232
|
+
const totalHours = effortHours(
|
|
233
|
+
critical.length,
|
|
234
|
+
serious.length,
|
|
235
|
+
moderate.length,
|
|
236
|
+
minor.length,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
function sprintBlock(label, items, hours, startIndex = 1) {
|
|
240
|
+
if (items.length === 0) return "";
|
|
241
|
+
const rows = items
|
|
242
|
+
.slice(0, 8)
|
|
243
|
+
.map(
|
|
244
|
+
(f, i) =>
|
|
245
|
+
`<tr><td style="width: 2cm; color: #6b7280;">#${startIndex + i}</td><td>${escapeHtml(f.title)}</td><td>${escapeHtml(f.area)}</td></tr>`,
|
|
246
|
+
)
|
|
247
|
+
.join("");
|
|
248
|
+
const more =
|
|
249
|
+
items.length > 8
|
|
250
|
+
? `<tr><td colspan="3" style="font-style: italic; color: #6b7280;">… and ${items.length - 8} more</td></tr>`
|
|
251
|
+
: "";
|
|
252
|
+
return `
|
|
253
|
+
<div style="margin-bottom: 1.5rem; page-break-inside: avoid;">
|
|
254
|
+
<p style="font-family: sans-serif; font-size: 9pt; font-weight: 800; text-transform: uppercase;
|
|
255
|
+
letter-spacing: 1pt; margin: 0 0 4pt 0; color: #6b7280;">
|
|
256
|
+
${label}
|
|
257
|
+
<span style="font-weight: 400; color: #9ca3af;"> — ${items.length} issue${items.length !== 1 ? "s" : ""} · ~${hours}h estimated</span>
|
|
258
|
+
</p>
|
|
259
|
+
<table class="stats-table" style="margin: 0;">
|
|
260
|
+
<thead><tr><th style="width: 2cm;">#</th><th>Issue</th><th>Page</th></tr></thead>
|
|
261
|
+
<tbody>${rows}${more}</tbody>
|
|
262
|
+
</table>
|
|
263
|
+
</div>`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return `
|
|
267
|
+
<div style="page-break-before: always;">
|
|
268
|
+
<h2 style="margin-top: 0;">3. Remediation Roadmap</h2>
|
|
269
|
+
<p style="line-height: 1.8; font-size: 10pt; margin-bottom: 1.2rem;">
|
|
270
|
+
Issues are prioritized by severity and grouped into recommended remediation sprints.
|
|
271
|
+
Effort estimates assume a developer familiar with the codebase.
|
|
272
|
+
Total estimated remediation effort: <strong>~${totalHours} hours</strong>.
|
|
273
|
+
</p>
|
|
274
|
+
|
|
275
|
+
${sprintBlock("Sprint 1 — Fix Immediately (Critical)", critical, effortHours(critical.length, 0, 0, 0), 1)}
|
|
276
|
+
${sprintBlock("Sprint 2 — Fix This Cycle (Serious)", serious, effortHours(0, serious.length, 0, 0), critical.length + 1)}
|
|
277
|
+
${sprintBlock("Sprint 3 — Fix Next Cycle (Moderate + Minor)", [...moderate, ...minor], effortHours(0, 0, moderate.length, minor.length), critical.length + serious.length + 1)}
|
|
278
|
+
|
|
279
|
+
${findings.length === 0 ? `<p style="font-style: italic; color: #6b7280;">No automated issues found. Complete the manual verification checklist in Section 5 to finalize the assessment.</p>` : ""}
|
|
280
|
+
</div>`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Builds the Methodology & Scope section for the PDF report.
|
|
285
|
+
* @param {Object} args - The parsed CLI arguments.
|
|
286
|
+
* @param {Object[]} findings - The normalized findings for scope calculation.
|
|
287
|
+
* @returns {string} The HTML string for the methodology section.
|
|
288
|
+
*/
|
|
289
|
+
export function buildPdfMethodologySection(args, findings) {
|
|
290
|
+
const pagesScanned = new Set(findings.map((f) => f.url)).size || 1;
|
|
291
|
+
const scope = args.scope || "Full Site Scan";
|
|
292
|
+
|
|
293
|
+
return `
|
|
294
|
+
<div style="page-break-before: always;">
|
|
295
|
+
<h2 style="margin-top: 0;">4. Methodology & Scope</h2>
|
|
296
|
+
<p style="font-size: 10pt; line-height: 1.7; margin-bottom: 1rem;">
|
|
297
|
+
This audit was conducted using automated accessibility testing tools against the live
|
|
298
|
+
environment of <strong>${escapeHtml(args.baseUrl)}</strong>. The methodology, tools, and
|
|
299
|
+
scope boundaries are documented below to ensure the results can be accurately interpreted.
|
|
300
|
+
</p>
|
|
301
|
+
|
|
302
|
+
<h3 style="font-size: 11pt; margin-bottom: 6pt;">Testing Approach</h3>
|
|
303
|
+
<table class="stats-table" style="margin-bottom: 1.2rem;">
|
|
304
|
+
<tbody>
|
|
305
|
+
<tr><td style="width: 35%; font-weight: 700;">Method</td><td>Automated scanning via axe-core engine injected into a live Chromium browser</td></tr>
|
|
306
|
+
<tr><td style="font-weight: 700;">Engine</td><td>axe-core 4.11.1 (Deque Systems) — industry-standard accessibility rules library</td></tr>
|
|
307
|
+
<tr><td style="font-weight: 700;">Browser</td><td>Chromium (headless) via Playwright</td></tr>
|
|
308
|
+
<tr><td style="font-weight: 700;">Standard</td><td>${escapeHtml(args.target)}</td></tr>
|
|
309
|
+
<tr><td style="font-weight: 700;">Audit date</td><td>${new Date().toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}</td></tr>
|
|
310
|
+
</tbody>
|
|
311
|
+
</table>
|
|
312
|
+
|
|
313
|
+
<h3 style="font-size: 11pt; margin-bottom: 6pt;">Scope</h3>
|
|
314
|
+
<table class="stats-table" style="margin-bottom: 1.2rem;">
|
|
315
|
+
<tbody>
|
|
316
|
+
<tr><td style="width: 35%; font-weight: 700;">Audit scope</td><td>${escapeHtml(scope)}</td></tr>
|
|
317
|
+
<tr><td style="font-weight: 700;">Base URL</td><td><a href="${escapeHtml(args.baseUrl)}">${escapeHtml(args.baseUrl)}</a></td></tr>
|
|
318
|
+
<tr><td style="font-weight: 700;">Pages scanned</td><td>${pagesScanned} route${pagesScanned !== 1 ? "s" : ""} (autodiscovered via same-origin link crawl)</td></tr>
|
|
319
|
+
<tr><td style="font-weight: 700;">Color scheme</td><td>Light mode (default)</td></tr>
|
|
320
|
+
</tbody>
|
|
321
|
+
</table>
|
|
322
|
+
|
|
323
|
+
<h3 style="font-size: 11pt; margin-bottom: 6pt;">Severity Definitions</h3>
|
|
324
|
+
<table class="stats-table">
|
|
325
|
+
<thead><tr><th>Level</th><th>Definition</th><th>Recommended action</th></tr></thead>
|
|
326
|
+
<tbody>
|
|
327
|
+
${COMPLIANCE_CONFIG.severityDefinitions.map((d) => `<tr><td><strong>${escapeHtml(d.level)}</strong></td><td>${escapeHtml(d.definition)}</td><td>${escapeHtml(d.action)}</td></tr>`).join("\n ")}
|
|
328
|
+
</tbody>
|
|
329
|
+
</table>
|
|
330
|
+
</div>`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Builds the Recommended Next Steps section for the PDF report.
|
|
335
|
+
* @param {Object[]} findings - The normalized findings for effort calculation.
|
|
336
|
+
* @param {Object<string, number>} totals - Summary counts per severity.
|
|
337
|
+
* @returns {string} The HTML string for the next steps section.
|
|
338
|
+
*/
|
|
339
|
+
export function buildPdfNextSteps(findings, totals) {
|
|
340
|
+
const critical = findings.filter((f) => f.severity === "Critical");
|
|
341
|
+
const serious = findings.filter((f) => f.severity === "Serious");
|
|
342
|
+
const moderate = findings.filter((f) => f.severity === "Moderate");
|
|
343
|
+
const minor = findings.filter((f) => f.severity === "Minor");
|
|
344
|
+
const mult = COMPLIANCE_CONFIG.effortMultipliers;
|
|
345
|
+
|
|
346
|
+
const steps = [];
|
|
347
|
+
|
|
348
|
+
if (critical.length > 0) {
|
|
349
|
+
const hrs = Math.round(critical.length * mult.Critical);
|
|
350
|
+
steps.push(`<li style="margin-bottom: 10pt;"><strong>Address Critical issues immediately</strong> — ${critical.length} issue${critical.length !== 1 ? "s" : ""}, ~${hrs}h estimated. These are complete barriers for assistive technology users and represent the highest legal exposure.</li>`);
|
|
351
|
+
}
|
|
352
|
+
if (serious.length > 0) {
|
|
353
|
+
const hrs = Math.round(serious.length * mult.Serious);
|
|
354
|
+
steps.push(`<li style="margin-bottom: 10pt;"><strong>Resolve Serious severity issues before the next release</strong> — ${serious.length} issue${serious.length !== 1 ? "s" : ""}, ~${hrs}h estimated. These create significant friction that forces affected users to abandon flows.</li>`);
|
|
355
|
+
}
|
|
356
|
+
if (moderate.length + minor.length > 0) {
|
|
357
|
+
const hrs = Math.round(moderate.length * mult.Moderate + minor.length * mult.Minor);
|
|
358
|
+
steps.push(`<li style="margin-bottom: 10pt;"><strong>Plan Moderate and Minor fixes for the next development cycle</strong> — ${moderate.length + minor.length} issue${moderate.length + minor.length !== 1 ? "s" : ""}, ~${hrs}h estimated. These affect specific user groups but are not immediate blockers.</li>`);
|
|
359
|
+
}
|
|
360
|
+
if (findings.length === 0) {
|
|
361
|
+
steps.push(`<li style="margin-bottom: 10pt;"><strong>No automated violations were detected.</strong> Complete the manual verification checklist to confirm full WCAG 2.2 AA conformance before certifying compliance.</li>`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
steps.push(`<li style="margin-bottom: 10pt;"><strong>Complete manual verification</strong> — The accompanying testing checklist covers the 41 WCAG 2.2 criteria that automated tools cannot detect, including keyboard navigation, screen reader compatibility, and media accessibility. This step is required before full conformance can be certified.</li>`);
|
|
365
|
+
steps.push(`<li style="margin-bottom: 10pt;"><strong>Schedule a follow-up audit</strong> — Accessibility requires ongoing maintenance. Re-audit after each major release, or at minimum quarterly, to catch regressions before they compound.</li>`);
|
|
366
|
+
|
|
367
|
+
return `
|
|
368
|
+
<div style="page-break-before: always;">
|
|
369
|
+
<h2 style="margin-top: 0;">6. Recommended Next Steps</h2>
|
|
370
|
+
<p style="font-size: 10pt; line-height: 1.7; margin-bottom: 1rem;">
|
|
371
|
+
Based on the findings in this audit, the following actions are recommended in priority order:
|
|
372
|
+
</p>
|
|
373
|
+
<ol style="font-size: 10pt; line-height: 1.8; padding-left: 1.5rem; margin: 0;">
|
|
374
|
+
${steps.join("\n ")}
|
|
375
|
+
</ol>
|
|
376
|
+
</div>`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Builds the Audit Scope & Limitations section for the PDF report.
|
|
381
|
+
* @returns {string} Reusable HTML block explaining automated audit limits.
|
|
382
|
+
*/
|
|
383
|
+
export function buildPdfAuditLimitations() {
|
|
384
|
+
return `
|
|
385
|
+
<div style="page-break-before: always;">
|
|
386
|
+
<h2 style="margin-top: 0;">7. Audit Scope & Limitations</h2>
|
|
387
|
+
<p style="font-size: 10pt; line-height: 1.7; margin-bottom: 1rem;">
|
|
388
|
+
Automated accessibility tools, including axe-core, reliably detect approximately 30–40% of
|
|
389
|
+
WCAG violations. The remaining 60–70% require human judgement, assistive technology testing,
|
|
390
|
+
and contextual evaluation that no automated tool can perform.
|
|
391
|
+
</p>
|
|
392
|
+
<p style="font-size: 10pt; line-height: 1.7; margin-bottom: 1.2rem;">
|
|
393
|
+
This report documents the results of automated testing only. The following areas are
|
|
394
|
+
<strong>outside the scope</strong> of this audit and require separate manual review
|
|
395
|
+
before full WCAG 2.2 AA conformance can be certified:
|
|
396
|
+
</p>
|
|
397
|
+
|
|
398
|
+
<table class="stats-table" style="margin-bottom: 1.2rem;">
|
|
399
|
+
<thead><tr><th>Out-of-scope area</th><th>Why it requires manual review</th></tr></thead>
|
|
400
|
+
<tbody>
|
|
401
|
+
<tr>
|
|
402
|
+
<td><strong>Authenticated pages</strong> (dashboards, account areas, checkout)</td>
|
|
403
|
+
<td>Automated tools cannot log in or maintain sessions — these pages were not scanned</td>
|
|
404
|
+
</tr>
|
|
405
|
+
<tr>
|
|
406
|
+
<td><strong>Dynamic & interactive components</strong> (modals, carousels, dropdowns)</td>
|
|
407
|
+
<td>Axe-core evaluates the page at load time; interactions triggered by user input require scripted or manual testing</td>
|
|
408
|
+
</tr>
|
|
409
|
+
<tr>
|
|
410
|
+
<td><strong>Screen reader compatibility</strong></td>
|
|
411
|
+
<td>Real screen reader testing (NVDA, JAWS, VoiceOver) is required to verify announced content and navigation flow</td>
|
|
412
|
+
</tr>
|
|
413
|
+
<tr>
|
|
414
|
+
<td><strong>Keyboard navigation flow</strong></td>
|
|
415
|
+
<td>Logical tab order and focus management must be verified by a human tester navigating without a mouse</td>
|
|
416
|
+
</tr>
|
|
417
|
+
<tr>
|
|
418
|
+
<td><strong>Alternative text quality</strong></td>
|
|
419
|
+
<td>Axe-core confirms alt text exists, but cannot evaluate whether the description is meaningful or accurate</td>
|
|
420
|
+
</tr>
|
|
421
|
+
<tr>
|
|
422
|
+
<td><strong>6 WCAG 2.2 manual-only criteria</strong></td>
|
|
423
|
+
<td>Focus Appearance (2.4.11), Dragging Movements (2.5.7), Target Size (2.5.8), Consistent Help (3.2.6), Redundant Entry (3.3.7), Accessible Authentication (3.3.8)</td>
|
|
424
|
+
</tr>
|
|
425
|
+
<tr>
|
|
426
|
+
<td><strong>Third-party content</strong> (chat widgets, maps, embedded iframes)</td>
|
|
427
|
+
<td>Accessibility of third-party components is the responsibility of the vendor and was not assessed</td>
|
|
428
|
+
</tr>
|
|
429
|
+
<tr>
|
|
430
|
+
<td><strong>PDF & document downloads</strong></td>
|
|
431
|
+
<td>Document accessibility requires separate evaluation using dedicated tools (Adobe Acrobat Accessibility Checker, PAC 2024)</td>
|
|
432
|
+
</tr>
|
|
433
|
+
</tbody>
|
|
434
|
+
</table>
|
|
435
|
+
|
|
436
|
+
<div style="padding: 1rem 1.2rem; border: 1.5pt solid #d97706; border-left: 5pt solid #d97706; background: #fffbeb; page-break-inside: avoid;">
|
|
437
|
+
<p style="font-family: sans-serif; font-size: 9pt; font-weight: 800; text-transform: uppercase; letter-spacing: 1pt; margin: 0 0 4pt 0; color: #92400e;">Recommendation</p>
|
|
438
|
+
<p style="font-size: 9pt; line-height: 1.7; margin: 0; color: #374151;">
|
|
439
|
+
To achieve verified WCAG 2.2 AA conformance, this automated audit should be complemented
|
|
440
|
+
with manual testing by an accessibility specialist, including screen reader testing,
|
|
441
|
+
keyboard-only navigation, and review of the 6 manual-only WCAG 2.2 criteria.
|
|
442
|
+
The accompanying manual testing checklist provides step-by-step verification guidance for your development team.
|
|
443
|
+
</p>
|
|
444
|
+
</div>
|
|
445
|
+
</div>`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Builds the cover page for the PDF report.
|
|
450
|
+
* @param {Object} options - Cover page configuration.
|
|
451
|
+
* @param {string} options.siteHostname - The hostname of the audited site.
|
|
452
|
+
* @param {string} options.target - The compliance target (e.g., WCAG 2.2 AA).
|
|
453
|
+
* @param {number} options.score - The final compliance score.
|
|
454
|
+
* @param {string} options.coverDate - The formatted date for the cover.
|
|
455
|
+
* @returns {string} The HTML string for the cover page.
|
|
456
|
+
*/
|
|
457
|
+
export function buildPdfCoverPage({ siteHostname, target, score, wcagStatus, coverDate }) {
|
|
458
|
+
const metrics = scoreMetrics(score);
|
|
459
|
+
const scoreColor =
|
|
460
|
+
wcagStatus === "Fail" ? "#dc2626" : score >= 75 ? "#16a34a" : score >= 55 ? "#d97706" : "#dc2626";
|
|
461
|
+
|
|
462
|
+
return `
|
|
463
|
+
<div class="cover-page">
|
|
464
|
+
<!-- Top accent line -->
|
|
465
|
+
<div style="border-top: 5pt solid #111827; padding-top: 1.3cm;">
|
|
466
|
+
<p style="font-family: 'Inter', sans-serif; font-size: 7pt; font-weight: 700; letter-spacing: 3.5pt; text-transform: uppercase; color: #9ca3af; margin: 0;">Accessibility Assessment</p>
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
<!-- Main content -->
|
|
470
|
+
<div style="flex: 1; padding: 2cm 0 1.5cm 0;">
|
|
471
|
+
<p style="font-family: 'Inter', sans-serif; font-size: 13pt; color: #6b7280; margin: 0 0 0.4cm 0; font-weight: 400;">${escapeHtml(siteHostname)}</p>
|
|
472
|
+
<h1 style="font-family: 'Inter', sans-serif !important; font-size: 38pt !important; font-weight: 900 !important; line-height: 1.08 !important; color: #111827 !important; margin: 0 0 1.8cm 0 !important; border: none !important; padding: 0 !important;">Web Accessibility<br>Audit</h1>
|
|
473
|
+
<div style="border-top: 1.5pt solid #111827; width: 4.5cm; margin-bottom: 1.5cm;"></div>
|
|
474
|
+
|
|
475
|
+
<!-- Score + meta -->
|
|
476
|
+
<div style="display: flex; align-items: flex-start;">
|
|
477
|
+
<div style="min-width: 7cm;">
|
|
478
|
+
<p style="font-family: 'Inter', sans-serif; font-size: 7pt; font-weight: 700; text-transform: uppercase; letter-spacing: 2.5pt; color: #9ca3af; margin: 0 0 5pt 0;">Compliance Score</p>
|
|
479
|
+
<p style="font-family: 'Inter', sans-serif; font-size: 40pt; font-weight: 900; line-height: 1; margin: 0; color: ${scoreColor};">${score}<span style="font-size: 16pt; font-weight: 400; color: #9ca3af;"> / 100</span></p>
|
|
480
|
+
<p style="font-family: 'Inter', sans-serif; font-size: 9.5pt; font-weight: 700; color: #374151; margin: 5pt 0 0 0;">${metrics.label} — ${metrics.risk}</p>
|
|
481
|
+
</div>
|
|
482
|
+
<div style="border-left: 1pt solid #e5e7eb; padding-left: 2cm;">
|
|
483
|
+
<p style="font-family: 'Inter', sans-serif; font-size: 7pt; font-weight: 700; text-transform: uppercase; letter-spacing: 2.5pt; color: #9ca3af; margin: 0 0 4pt 0;">Standard</p>
|
|
484
|
+
<p style="font-family: 'Inter', sans-serif; font-size: 11pt; font-weight: 700; color: #111827; margin: 0 0 1.1cm 0;">${escapeHtml(target)}</p>
|
|
485
|
+
<p style="font-family: 'Inter', sans-serif; font-size: 7pt; font-weight: 700; text-transform: uppercase; letter-spacing: 2.5pt; color: #9ca3af; margin: 0 0 4pt 0;">Audit Date</p>
|
|
486
|
+
<p style="font-family: 'Inter', sans-serif; font-size: 11pt; font-weight: 700; color: #111827; margin: 0;">${escapeHtml(coverDate)}</p>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
<!-- Footer -->
|
|
492
|
+
<div style="border-top: 1pt solid #e5e7eb; padding-top: 0.6cm;">
|
|
493
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4pt;">
|
|
494
|
+
<p style="font-family: 'Inter', sans-serif; font-size: 8pt; color: #9ca3af; margin: 0;">Generated by <strong style="color: #6b7280;">a11y</strong></p>
|
|
495
|
+
<p style="font-family: 'Inter', sans-serif; font-size: 8pt; color: #9ca3af; margin: 0;">github.com/diegovelasquezweb/a11y</p>
|
|
496
|
+
</div>
|
|
497
|
+
<p style="font-family: 'Inter', sans-serif; font-size: 7.5pt; color: #9ca3af; margin: 0; font-style: italic;">Confidential — This report is intended solely for the organization that commissioned this audit. Do not distribute without authorization.</p>
|
|
498
|
+
</div>
|
|
499
|
+
</div>`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Builds the detailed issue summary table for the PDF report.
|
|
504
|
+
* @param {Object[]} findings - The list of findings to display in the table.
|
|
505
|
+
* @returns {string} The HTML string for the issue summary section.
|
|
506
|
+
*/
|
|
507
|
+
export function buildPdfIssueSummaryTable(findings) {
|
|
508
|
+
if (findings.length === 0) {
|
|
509
|
+
return `
|
|
510
|
+
<div style="page-break-before: always;">
|
|
511
|
+
<h2 style="margin-top: 0;">5. Issue Summary</h2>
|
|
512
|
+
<p style="font-size: 10pt; line-height: 1.7; margin-bottom: 1rem;">
|
|
513
|
+
The table below lists all accessibility issues detected during the automated scan.
|
|
514
|
+
Each issue is identified by severity, affected page, and the user groups it impacts.
|
|
515
|
+
Full technical detail for developers is provided in the accompanying HTML report.
|
|
516
|
+
</p>
|
|
517
|
+
<p style="font-style: italic; color: #6b7280; font-size: 10pt;">No accessibility violations were detected during the automated scan.</p>
|
|
518
|
+
</div>`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const rows = findings
|
|
522
|
+
.map(
|
|
523
|
+
(f, i) => `
|
|
524
|
+
<tr>
|
|
525
|
+
<td style="color: #6b7280; white-space: nowrap;">#${i + 1}</td>
|
|
526
|
+
<td>${escapeHtml(f.title)}</td>
|
|
527
|
+
<td style="white-space: nowrap;">${escapeHtml(f.area)}</td>
|
|
528
|
+
<td style="font-weight: 700; white-space: nowrap;">${escapeHtml(f.severity)}</td>
|
|
529
|
+
<td>${escapeHtml(f.impactedUsers)}</td>
|
|
530
|
+
</tr>`,
|
|
531
|
+
)
|
|
532
|
+
.join("");
|
|
533
|
+
|
|
534
|
+
return `
|
|
535
|
+
<div style="page-break-before: always;">
|
|
536
|
+
<h2 style="margin-top: 0;">5. Issue Summary</h2>
|
|
537
|
+
<p style="font-size: 10pt; line-height: 1.7; margin-bottom: 1rem;">
|
|
538
|
+
The table below lists all accessibility issues detected during the automated scan.
|
|
539
|
+
Each issue is identified by severity, affected page, and the user groups it impacts.
|
|
540
|
+
Full technical detail for developers is provided in the accompanying HTML report.
|
|
541
|
+
</p>
|
|
542
|
+
<table class="stats-table">
|
|
543
|
+
<thead>
|
|
544
|
+
<tr><th style="width: 1.5cm;">#</th><th>Issue</th><th>Page</th><th>Severity</th><th>Users Affected</th></tr>
|
|
545
|
+
</thead>
|
|
546
|
+
<tbody>
|
|
547
|
+
${rows}
|
|
548
|
+
</tbody>
|
|
549
|
+
</table>
|
|
550
|
+
</div>`;
|
|
551
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file utils.mjs
|
|
3
|
+
* @description Shared formatting and string manipulation utilities for report generation.
|
|
4
|
+
* Includes HTML escaping, multiline formatting, and automatic URL linkification.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Escapes special HTML characters in a string to prevent XSS and ensure proper rendering.
|
|
9
|
+
* @param {string} value - The raw string to escape.
|
|
10
|
+
* @returns {string} The HTML-escaped string.
|
|
11
|
+
*/
|
|
12
|
+
export function escapeHtml(value) {
|
|
13
|
+
return String(value ?? "")
|
|
14
|
+
.replaceAll("&", "&")
|
|
15
|
+
.replaceAll("<", "<")
|
|
16
|
+
.replaceAll(">", ">")
|
|
17
|
+
.replaceAll('"', """)
|
|
18
|
+
.replaceAll("'", "'");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Formats a multiline string for display in HTML by escaping it and converting newlines to <br> tags.
|
|
23
|
+
* @param {string} value - The multiline text to format.
|
|
24
|
+
* @returns {string} The formatted HTML string.
|
|
25
|
+
*/
|
|
26
|
+
export function formatMultiline(value) {
|
|
27
|
+
return escapeHtml(value).replace(/\r?\n/g, "<br>");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Identifies URLs in a text string and converts them into clickable <a> anchor tags.
|
|
32
|
+
* Optimized for Tailwind-based or similar styling contexts.
|
|
33
|
+
* @param {string} text - The text containing potential URLs.
|
|
34
|
+
* @returns {string} The text with URLs converted to HTML links.
|
|
35
|
+
*/
|
|
36
|
+
export function linkify(text) {
|
|
37
|
+
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
|
38
|
+
return text.replace(
|
|
39
|
+
urlRegex,
|
|
40
|
+
'<a href="$1" target="_blank" class="text-indigo-600 hover:underline font-medium break-all">$1</a>',
|
|
41
|
+
);
|
|
42
|
+
}
|