@hypoth-ui/a11y-audit 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 +47 -0
- package/dist/audit-Q3UXBYIW.js +266 -0
- package/dist/chunk-AMA6KCPL.js +39 -0
- package/dist/chunk-KLSGW6PX.js +515 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +36 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +24 -0
- package/dist/report-6GBLBDLV.js +587 -0
- package/dist/validate-MQQMU57I.js +112 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_SEVERITY_THRESHOLD,
|
|
3
|
+
SEVERITY_ENV_VAR,
|
|
4
|
+
SEVERITY_LEVELS,
|
|
5
|
+
describeSeverityThreshold,
|
|
6
|
+
parseSeverityThreshold,
|
|
7
|
+
shouldFailOnSeverity
|
|
8
|
+
} from "./chunk-AMA6KCPL.js";
|
|
9
|
+
import {
|
|
10
|
+
validateChecklist,
|
|
11
|
+
validateRecord,
|
|
12
|
+
validateReport
|
|
13
|
+
} from "./chunk-KLSGW6PX.js";
|
|
14
|
+
export {
|
|
15
|
+
DEFAULT_SEVERITY_THRESHOLD,
|
|
16
|
+
SEVERITY_ENV_VAR,
|
|
17
|
+
SEVERITY_LEVELS,
|
|
18
|
+
describeSeverityThreshold,
|
|
19
|
+
parseSeverityThreshold,
|
|
20
|
+
shouldFailOnSeverity,
|
|
21
|
+
validateChecklist,
|
|
22
|
+
validateRecord,
|
|
23
|
+
validateReport
|
|
24
|
+
};
|
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateReport
|
|
3
|
+
} from "./chunk-KLSGW6PX.js";
|
|
4
|
+
|
|
5
|
+
// src/cli/report.ts
|
|
6
|
+
import * as fs3 from "fs";
|
|
7
|
+
import * as path3 from "path";
|
|
8
|
+
|
|
9
|
+
// src/lib/aggregator.ts
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
function loadAuditRecords(recordsDir) {
|
|
13
|
+
const records = /* @__PURE__ */ new Map();
|
|
14
|
+
if (!fs.existsSync(recordsDir)) {
|
|
15
|
+
return records;
|
|
16
|
+
}
|
|
17
|
+
const components = fs.readdirSync(recordsDir, { withFileTypes: true });
|
|
18
|
+
for (const component of components) {
|
|
19
|
+
if (!component.isDirectory()) continue;
|
|
20
|
+
const componentPath = path.join(recordsDir, component.name);
|
|
21
|
+
const files = fs.readdirSync(componentPath).filter((f) => f.endsWith(".json") && !f.includes(".backup"));
|
|
22
|
+
const componentRecords = [];
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
try {
|
|
25
|
+
const content = fs.readFileSync(path.join(componentPath, file), "utf-8");
|
|
26
|
+
const record = JSON.parse(content);
|
|
27
|
+
componentRecords.push(record);
|
|
28
|
+
} catch {
|
|
29
|
+
console.warn(`Warning: Could not parse ${path.join(componentPath, file)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (componentRecords.length > 0) {
|
|
33
|
+
records.set(component.name, componentRecords);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return records;
|
|
37
|
+
}
|
|
38
|
+
function getLatestRecord(records, version) {
|
|
39
|
+
if (version) {
|
|
40
|
+
return records.find((r) => r.version === version);
|
|
41
|
+
}
|
|
42
|
+
return records.sort(
|
|
43
|
+
(a, b) => new Date(b.auditDate).getTime() - new Date(a.auditDate).getTime()
|
|
44
|
+
)[0];
|
|
45
|
+
}
|
|
46
|
+
function calculateConformanceStatus(data) {
|
|
47
|
+
const hasAutomated = data.automatedResult !== void 0;
|
|
48
|
+
const hasManual = data.manualAudit !== void 0;
|
|
49
|
+
if (!hasAutomated && !hasManual) {
|
|
50
|
+
return "pending";
|
|
51
|
+
}
|
|
52
|
+
if (hasAutomated && !hasManual) {
|
|
53
|
+
return "pending";
|
|
54
|
+
}
|
|
55
|
+
if (hasAutomated && data.automatedResult?.passed === false) {
|
|
56
|
+
return "non-conformant";
|
|
57
|
+
}
|
|
58
|
+
if (hasManual && data.manualAudit) {
|
|
59
|
+
switch (data.manualAudit.overallStatus) {
|
|
60
|
+
case "conformant":
|
|
61
|
+
return "conformant";
|
|
62
|
+
case "partial":
|
|
63
|
+
return "partial";
|
|
64
|
+
case "non-conformant":
|
|
65
|
+
return "non-conformant";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return "pending";
|
|
69
|
+
}
|
|
70
|
+
function createManualAuditSummary(record) {
|
|
71
|
+
const passCount = record.items.filter((i) => i.status === "pass").length;
|
|
72
|
+
const failCount = record.items.filter((i) => i.status === "fail").length;
|
|
73
|
+
const exceptionCount = record.exceptions?.length ?? 0;
|
|
74
|
+
return {
|
|
75
|
+
status: record.overallStatus,
|
|
76
|
+
auditId: record.id,
|
|
77
|
+
auditor: record.auditor,
|
|
78
|
+
auditDate: record.auditDate,
|
|
79
|
+
passCount,
|
|
80
|
+
failCount,
|
|
81
|
+
exceptionCount
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function createComponentStatus(data) {
|
|
85
|
+
const status = calculateConformanceStatus(data);
|
|
86
|
+
const automatedResult = data.automatedResult ?? {
|
|
87
|
+
passed: true,
|
|
88
|
+
violationCount: 0,
|
|
89
|
+
runId: "pending"
|
|
90
|
+
};
|
|
91
|
+
const lastUpdated = data.manualAudit?.auditDate ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
92
|
+
return {
|
|
93
|
+
component: data.component,
|
|
94
|
+
version: data.version,
|
|
95
|
+
status,
|
|
96
|
+
automatedResult,
|
|
97
|
+
manualAudit: data.manualAudit ? createManualAuditSummary(data.manualAudit) : void 0,
|
|
98
|
+
lastUpdated
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function aggregateForRelease(components, recordsDir, version) {
|
|
102
|
+
const allRecords = loadAuditRecords(recordsDir);
|
|
103
|
+
const statuses = [];
|
|
104
|
+
for (const component of components) {
|
|
105
|
+
const records = allRecords.get(component) ?? [];
|
|
106
|
+
const latestRecord = getLatestRecord(records, version) ?? getLatestRecord(records);
|
|
107
|
+
const data = {
|
|
108
|
+
component,
|
|
109
|
+
version,
|
|
110
|
+
manualAudit: latestRecord
|
|
111
|
+
};
|
|
112
|
+
statuses.push(createComponentStatus(data));
|
|
113
|
+
}
|
|
114
|
+
return statuses;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/lib/manifest-loader.ts
|
|
118
|
+
import * as fs2 from "fs";
|
|
119
|
+
import * as path2 from "path";
|
|
120
|
+
var DEFAULT_COMPONENTS = [
|
|
121
|
+
"ds-button",
|
|
122
|
+
"ds-checkbox",
|
|
123
|
+
"ds-dialog",
|
|
124
|
+
"ds-field",
|
|
125
|
+
"ds-icon",
|
|
126
|
+
"ds-input",
|
|
127
|
+
"ds-link",
|
|
128
|
+
"ds-menu",
|
|
129
|
+
"ds-popover",
|
|
130
|
+
"ds-radio",
|
|
131
|
+
"ds-spinner",
|
|
132
|
+
"ds-switch",
|
|
133
|
+
"ds-text",
|
|
134
|
+
"ds-textarea",
|
|
135
|
+
"ds-tooltip",
|
|
136
|
+
"ds-visually-hidden"
|
|
137
|
+
];
|
|
138
|
+
function loadComponentList(manifestPath) {
|
|
139
|
+
if (manifestPath && fs2.existsSync(manifestPath)) {
|
|
140
|
+
try {
|
|
141
|
+
const content = fs2.readFileSync(manifestPath, "utf-8");
|
|
142
|
+
const manifest = JSON.parse(content);
|
|
143
|
+
if (Array.isArray(manifest.components)) {
|
|
144
|
+
return manifest.components.map(
|
|
145
|
+
(c) => typeof c === "string" ? c : c.id
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(manifest)) {
|
|
149
|
+
return manifest.map((c) => typeof c === "string" ? c : c.id);
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
console.warn(`Warning: Could not parse manifest at ${manifestPath}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const wcComponentsPath = path2.resolve(process.cwd(), "packages/wc/src/components");
|
|
156
|
+
if (fs2.existsSync(wcComponentsPath)) {
|
|
157
|
+
const dirs = fs2.readdirSync(wcComponentsPath, { withFileTypes: true });
|
|
158
|
+
const components = dirs.filter((d) => d.isDirectory()).map((d) => `ds-${d.name}`);
|
|
159
|
+
if (components.length > 0) {
|
|
160
|
+
return components;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return DEFAULT_COMPONENTS;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/lib/report-html.ts
|
|
167
|
+
import Handlebars from "handlebars";
|
|
168
|
+
Handlebars.registerHelper("statusIcon", (status) => {
|
|
169
|
+
switch (status) {
|
|
170
|
+
case "conformant":
|
|
171
|
+
return "\u2705";
|
|
172
|
+
case "partial":
|
|
173
|
+
return "\u26A0\uFE0F";
|
|
174
|
+
case "non-conformant":
|
|
175
|
+
return "\u274C";
|
|
176
|
+
case "pending":
|
|
177
|
+
return "\u23F3";
|
|
178
|
+
default:
|
|
179
|
+
return "\u2753";
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
Handlebars.registerHelper("statusClass", (status) => {
|
|
183
|
+
switch (status) {
|
|
184
|
+
case "conformant":
|
|
185
|
+
return "status-conformant";
|
|
186
|
+
case "partial":
|
|
187
|
+
return "status-partial";
|
|
188
|
+
case "non-conformant":
|
|
189
|
+
return "status-non-conformant";
|
|
190
|
+
case "pending":
|
|
191
|
+
return "status-pending";
|
|
192
|
+
default:
|
|
193
|
+
return "";
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
Handlebars.registerHelper(
|
|
197
|
+
"formatDate",
|
|
198
|
+
(dateString) => new Date(dateString).toLocaleDateString("en-US", {
|
|
199
|
+
year: "numeric",
|
|
200
|
+
month: "long",
|
|
201
|
+
day: "numeric",
|
|
202
|
+
hour: "2-digit",
|
|
203
|
+
minute: "2-digit"
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
var HTML_TEMPLATE = `<!DOCTYPE html>
|
|
207
|
+
<html lang="en">
|
|
208
|
+
<head>
|
|
209
|
+
<meta charset="UTF-8">
|
|
210
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
211
|
+
<title>Accessibility Conformance Report - v{{version}}</title>
|
|
212
|
+
<style>
|
|
213
|
+
:root {
|
|
214
|
+
--color-conformant: #22c55e;
|
|
215
|
+
--color-partial: #f59e0b;
|
|
216
|
+
--color-non-conformant: #ef4444;
|
|
217
|
+
--color-pending: #6b7280;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
* {
|
|
221
|
+
box-sizing: border-box;
|
|
222
|
+
margin: 0;
|
|
223
|
+
padding: 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
body {
|
|
227
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
228
|
+
line-height: 1.6;
|
|
229
|
+
color: #1f2937;
|
|
230
|
+
max-width: 1200px;
|
|
231
|
+
margin: 0 auto;
|
|
232
|
+
padding: 2rem;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
header {
|
|
236
|
+
border-bottom: 2px solid #e5e7eb;
|
|
237
|
+
padding-bottom: 1.5rem;
|
|
238
|
+
margin-bottom: 2rem;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
h1 {
|
|
242
|
+
font-size: 2rem;
|
|
243
|
+
margin-bottom: 0.5rem;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.meta {
|
|
247
|
+
color: #6b7280;
|
|
248
|
+
font-size: 0.875rem;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.summary {
|
|
252
|
+
display: grid;
|
|
253
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
254
|
+
gap: 1rem;
|
|
255
|
+
margin-bottom: 2rem;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.summary-card {
|
|
259
|
+
background: #f9fafb;
|
|
260
|
+
border-radius: 8px;
|
|
261
|
+
padding: 1.5rem;
|
|
262
|
+
text-align: center;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.summary-card .number {
|
|
266
|
+
font-size: 2.5rem;
|
|
267
|
+
font-weight: bold;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.summary-card .label {
|
|
271
|
+
color: #6b7280;
|
|
272
|
+
font-size: 0.875rem;
|
|
273
|
+
text-transform: uppercase;
|
|
274
|
+
letter-spacing: 0.05em;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.status-conformant .number { color: var(--color-conformant); }
|
|
278
|
+
.status-partial .number { color: var(--color-partial); }
|
|
279
|
+
.status-non-conformant .number { color: var(--color-non-conformant); }
|
|
280
|
+
.status-pending .number { color: var(--color-pending); }
|
|
281
|
+
|
|
282
|
+
table {
|
|
283
|
+
width: 100%;
|
|
284
|
+
border-collapse: collapse;
|
|
285
|
+
margin-bottom: 2rem;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
th, td {
|
|
289
|
+
padding: 1rem;
|
|
290
|
+
text-align: left;
|
|
291
|
+
border-bottom: 1px solid #e5e7eb;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
th {
|
|
295
|
+
background: #f9fafb;
|
|
296
|
+
font-weight: 600;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
tr:hover {
|
|
300
|
+
background: #f9fafb;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.status-badge {
|
|
304
|
+
display: inline-flex;
|
|
305
|
+
align-items: center;
|
|
306
|
+
gap: 0.5rem;
|
|
307
|
+
padding: 0.25rem 0.75rem;
|
|
308
|
+
border-radius: 9999px;
|
|
309
|
+
font-size: 0.875rem;
|
|
310
|
+
font-weight: 500;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.status-badge.status-conformant {
|
|
314
|
+
background: #dcfce7;
|
|
315
|
+
color: #166534;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.status-badge.status-partial {
|
|
319
|
+
background: #fef3c7;
|
|
320
|
+
color: #92400e;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.status-badge.status-non-conformant {
|
|
324
|
+
background: #fee2e2;
|
|
325
|
+
color: #991b1b;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.status-badge.status-pending {
|
|
329
|
+
background: #f3f4f6;
|
|
330
|
+
color: #374151;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.progress-bar {
|
|
334
|
+
height: 8px;
|
|
335
|
+
background: #e5e7eb;
|
|
336
|
+
border-radius: 4px;
|
|
337
|
+
overflow: hidden;
|
|
338
|
+
margin-top: 0.5rem;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.progress-fill {
|
|
342
|
+
height: 100%;
|
|
343
|
+
background: var(--color-conformant);
|
|
344
|
+
transition: width 0.3s ease;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
footer {
|
|
348
|
+
border-top: 1px solid #e5e7eb;
|
|
349
|
+
padding-top: 1rem;
|
|
350
|
+
margin-top: 2rem;
|
|
351
|
+
color: #6b7280;
|
|
352
|
+
font-size: 0.875rem;
|
|
353
|
+
}
|
|
354
|
+
</style>
|
|
355
|
+
</head>
|
|
356
|
+
<body>
|
|
357
|
+
<header>
|
|
358
|
+
<h1>Accessibility Conformance Report</h1>
|
|
359
|
+
<p class="meta">
|
|
360
|
+
Version {{version}} \u2022 Generated {{formatDate generatedAt}} \u2022 WCAG {{wcagVersion}} Level {{conformanceLevel}}
|
|
361
|
+
</p>
|
|
362
|
+
</header>
|
|
363
|
+
|
|
364
|
+
<section class="summary">
|
|
365
|
+
<div class="summary-card">
|
|
366
|
+
<div class="number">{{summary.totalComponents}}</div>
|
|
367
|
+
<div class="label">Total Components</div>
|
|
368
|
+
</div>
|
|
369
|
+
<div class="summary-card status-conformant">
|
|
370
|
+
<div class="number">{{summary.conformant}}</div>
|
|
371
|
+
<div class="label">Conformant</div>
|
|
372
|
+
</div>
|
|
373
|
+
<div class="summary-card status-partial">
|
|
374
|
+
<div class="number">{{summary.partial}}</div>
|
|
375
|
+
<div class="label">Partial</div>
|
|
376
|
+
</div>
|
|
377
|
+
<div class="summary-card status-non-conformant">
|
|
378
|
+
<div class="number">{{summary.nonConformant}}</div>
|
|
379
|
+
<div class="label">Non-Conformant</div>
|
|
380
|
+
</div>
|
|
381
|
+
<div class="summary-card status-pending">
|
|
382
|
+
<div class="number">{{summary.pending}}</div>
|
|
383
|
+
<div class="label">Pending Audit</div>
|
|
384
|
+
</div>
|
|
385
|
+
</section>
|
|
386
|
+
|
|
387
|
+
<section>
|
|
388
|
+
<h2>Conformance Progress</h2>
|
|
389
|
+
<p>{{summary.conformancePercentage}}% of components are fully conformant</p>
|
|
390
|
+
<div class="progress-bar">
|
|
391
|
+
<div class="progress-fill" style="width: {{summary.conformancePercentage}}%"></div>
|
|
392
|
+
</div>
|
|
393
|
+
</section>
|
|
394
|
+
|
|
395
|
+
<section style="margin-top: 2rem;">
|
|
396
|
+
<h2>Component Status</h2>
|
|
397
|
+
<table>
|
|
398
|
+
<thead>
|
|
399
|
+
<tr>
|
|
400
|
+
<th>Component</th>
|
|
401
|
+
<th>Version</th>
|
|
402
|
+
<th>Status</th>
|
|
403
|
+
<th>Automated</th>
|
|
404
|
+
<th>Manual Audit</th>
|
|
405
|
+
<th>Last Updated</th>
|
|
406
|
+
</tr>
|
|
407
|
+
</thead>
|
|
408
|
+
<tbody>
|
|
409
|
+
{{#each components}}
|
|
410
|
+
<tr>
|
|
411
|
+
<td><strong>{{component}}</strong></td>
|
|
412
|
+
<td>{{version}}</td>
|
|
413
|
+
<td>
|
|
414
|
+
<span class="status-badge {{statusClass status}}">
|
|
415
|
+
{{statusIcon status}} {{status}}
|
|
416
|
+
</span>
|
|
417
|
+
</td>
|
|
418
|
+
<td>
|
|
419
|
+
{{#if automatedResult.passed}}
|
|
420
|
+
\u2705 Passed
|
|
421
|
+
{{else}}
|
|
422
|
+
\u274C {{automatedResult.violationCount}} violations
|
|
423
|
+
{{/if}}
|
|
424
|
+
</td>
|
|
425
|
+
<td>
|
|
426
|
+
{{#if manualAudit}}
|
|
427
|
+
{{manualAudit.passCount}}/{{manualAudit.passCount}} passed
|
|
428
|
+
{{else}}
|
|
429
|
+
<em>Not audited</em>
|
|
430
|
+
{{/if}}
|
|
431
|
+
</td>
|
|
432
|
+
<td>{{formatDate lastUpdated}}</td>
|
|
433
|
+
</tr>
|
|
434
|
+
{{/each}}
|
|
435
|
+
</tbody>
|
|
436
|
+
</table>
|
|
437
|
+
</section>
|
|
438
|
+
|
|
439
|
+
<footer>
|
|
440
|
+
<p>Generated by @hypoth-ui/a11y-audit v{{metadata.toolVersion}}</p>
|
|
441
|
+
{{#if metadata.gitCommit}}
|
|
442
|
+
<p>Git commit: {{metadata.gitCommit}}</p>
|
|
443
|
+
{{/if}}
|
|
444
|
+
{{#if metadata.ciRunUrl}}
|
|
445
|
+
<p>CI Run: <a href="{{metadata.ciRunUrl}}">View details</a></p>
|
|
446
|
+
{{/if}}
|
|
447
|
+
</footer>
|
|
448
|
+
</body>
|
|
449
|
+
</html>`;
|
|
450
|
+
var template = Handlebars.compile(HTML_TEMPLATE);
|
|
451
|
+
function generateHTMLReport(report2) {
|
|
452
|
+
return template(report2);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/lib/report-json.ts
|
|
456
|
+
import { randomUUID } from "crypto";
|
|
457
|
+
function calculateSummary(components) {
|
|
458
|
+
const total = components.length;
|
|
459
|
+
const conformant = components.filter((c) => c.status === "conformant").length;
|
|
460
|
+
const partial = components.filter((c) => c.status === "partial").length;
|
|
461
|
+
const nonConformant = components.filter((c) => c.status === "non-conformant").length;
|
|
462
|
+
const pending = components.filter((c) => c.status === "pending").length;
|
|
463
|
+
const conformancePercentage = total > 0 ? Math.round(conformant / total * 100) : 0;
|
|
464
|
+
return {
|
|
465
|
+
totalComponents: total,
|
|
466
|
+
conformant,
|
|
467
|
+
partial,
|
|
468
|
+
nonConformant,
|
|
469
|
+
pending,
|
|
470
|
+
conformancePercentage
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function generateJSONReport(options) {
|
|
474
|
+
const metadata = {
|
|
475
|
+
schemaVersion: "1.0.0",
|
|
476
|
+
toolVersion: "0.0.0",
|
|
477
|
+
axeCoreVersion: options.axeCoreVersion,
|
|
478
|
+
gitCommit: options.gitCommit,
|
|
479
|
+
ciRunUrl: options.ciRunUrl
|
|
480
|
+
};
|
|
481
|
+
const report2 = {
|
|
482
|
+
id: randomUUID(),
|
|
483
|
+
version: options.version,
|
|
484
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
485
|
+
generatedBy: options.generatedBy,
|
|
486
|
+
wcagVersion: options.wcagVersion ?? "2.1",
|
|
487
|
+
conformanceLevel: options.conformanceLevel ?? "AA",
|
|
488
|
+
components: options.components,
|
|
489
|
+
summary: calculateSummary(options.components),
|
|
490
|
+
metadata
|
|
491
|
+
};
|
|
492
|
+
return report2;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/cli/report.ts
|
|
496
|
+
async function report(options) {
|
|
497
|
+
const { version, output, format } = options;
|
|
498
|
+
if (!version.match(/^\d+\.\d+\.\d+$/)) {
|
|
499
|
+
console.error(`\u274C Invalid version format: ${version}`);
|
|
500
|
+
console.error(" Use semver format: 1.0.0, 2.1.0, etc.");
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
const formats = format.split(",").map((f) => f.trim().toLowerCase());
|
|
504
|
+
const validFormats = ["json", "html"];
|
|
505
|
+
for (const fmt of formats) {
|
|
506
|
+
if (!validFormats.includes(fmt)) {
|
|
507
|
+
console.error(`\u274C Invalid format: ${fmt}`);
|
|
508
|
+
console.error(` Valid formats: ${validFormats.join(", ")}`);
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
console.info(`
|
|
513
|
+
\u{1F50D} Generating conformance report
|
|
514
|
+
Version: ${version}
|
|
515
|
+
Output: ${output}
|
|
516
|
+
Formats: ${formats.join(", ")}
|
|
517
|
+
`);
|
|
518
|
+
const components = loadComponentList();
|
|
519
|
+
console.info(`Found ${components.length} components`);
|
|
520
|
+
const recordsDir = path3.resolve(process.cwd(), "a11y-audits/records");
|
|
521
|
+
const componentStatuses = aggregateForRelease(components, recordsDir, version);
|
|
522
|
+
const jsonReport = generateJSONReport({
|
|
523
|
+
version,
|
|
524
|
+
generatedBy: process.env.GITHUB_RUN_ID ?? "manual",
|
|
525
|
+
components: componentStatuses,
|
|
526
|
+
gitCommit: process.env.GITHUB_SHA,
|
|
527
|
+
ciRunUrl: process.env.GITHUB_RUN_ID ? `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` : void 0
|
|
528
|
+
});
|
|
529
|
+
const validation = validateReport(jsonReport);
|
|
530
|
+
if (!validation.valid) {
|
|
531
|
+
console.error("\u274C Generated report failed validation:");
|
|
532
|
+
for (const error of validation.errors) {
|
|
533
|
+
console.error(` - ${error}`);
|
|
534
|
+
}
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
const outputDir = path3.resolve(process.cwd(), output, version);
|
|
538
|
+
if (!fs3.existsSync(outputDir)) {
|
|
539
|
+
fs3.mkdirSync(outputDir, { recursive: true });
|
|
540
|
+
}
|
|
541
|
+
const outputs = [];
|
|
542
|
+
if (formats.includes("json")) {
|
|
543
|
+
const jsonPath = path3.join(outputDir, "report.json");
|
|
544
|
+
fs3.writeFileSync(jsonPath, JSON.stringify(jsonReport, null, 2));
|
|
545
|
+
outputs.push(jsonPath);
|
|
546
|
+
console.info(` \u2705 JSON: ${jsonPath}`);
|
|
547
|
+
}
|
|
548
|
+
if (formats.includes("html")) {
|
|
549
|
+
const htmlContent = generateHTMLReport(jsonReport);
|
|
550
|
+
const htmlPath = path3.join(outputDir, "report.html");
|
|
551
|
+
fs3.writeFileSync(htmlPath, htmlContent);
|
|
552
|
+
outputs.push(htmlPath);
|
|
553
|
+
console.info(` \u2705 HTML: ${htmlPath}`);
|
|
554
|
+
}
|
|
555
|
+
const { summary } = jsonReport;
|
|
556
|
+
console.info(`
|
|
557
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
558
|
+
\u2551 CONFORMANCE REPORT GENERATED \u2551
|
|
559
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
560
|
+
\u2551 Version: ${version.padEnd(51)}\u2551
|
|
561
|
+
\u2551 WCAG: ${jsonReport.wcagVersion} Level ${jsonReport.conformanceLevel.padEnd(43)}\u2551
|
|
562
|
+
\u2551 \u2551
|
|
563
|
+
\u2551 Summary: \u2551
|
|
564
|
+
\u2551 Total Components: ${String(summary.totalComponents).padEnd(42)}\u2551
|
|
565
|
+
\u2551 \u2705 Conformant: ${String(summary.conformant).padEnd(42)}\u2551
|
|
566
|
+
\u2551 \u26A0\uFE0F Partial: ${String(summary.partial).padEnd(42)}\u2551
|
|
567
|
+
\u2551 \u274C Non-Conformant: ${String(summary.nonConformant).padEnd(42)}\u2551
|
|
568
|
+
\u2551 \u23F3 Pending: ${String(summary.pending).padEnd(42)}\u2551
|
|
569
|
+
\u2551 \u2551
|
|
570
|
+
\u2551 Conformance: ${String(`${summary.conformancePercentage}%`).padEnd(48)}\u2551
|
|
571
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
572
|
+
`);
|
|
573
|
+
if (summary.nonConformant > 0 || summary.pending > 0) {
|
|
574
|
+
console.info("\u26A0\uFE0F Release gate check:");
|
|
575
|
+
if (summary.nonConformant > 0) {
|
|
576
|
+
console.info(` \u274C ${summary.nonConformant} component(s) are non-conformant`);
|
|
577
|
+
}
|
|
578
|
+
if (summary.pending > 0) {
|
|
579
|
+
console.info(` \u23F3 ${summary.pending} component(s) pending manual audit`);
|
|
580
|
+
}
|
|
581
|
+
console.info("");
|
|
582
|
+
console.info(" Complete manual audits before release to achieve full conformance.");
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
export {
|
|
586
|
+
report
|
|
587
|
+
};
|