@dallask/a11y-mcp-srv 1.0.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/NOTICE +9 -0
- package/README.md +1328 -0
- package/bin/server.js +8 -0
- package/dist/core/accessibility-runner.d.ts +123 -0
- package/dist/core/accessibility-runner.d.ts.map +1 -0
- package/dist/core/accessibility-runner.js +465 -0
- package/dist/core/accessibility-runner.js.map +1 -0
- package/dist/core/basic-auth.d.ts +35 -0
- package/dist/core/basic-auth.d.ts.map +1 -0
- package/dist/core/basic-auth.js +52 -0
- package/dist/core/basic-auth.js.map +1 -0
- package/dist/core/config.d.ts +44 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +163 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/error-handler.d.ts +66 -0
- package/dist/core/error-handler.d.ts.map +1 -0
- package/dist/core/error-handler.js +305 -0
- package/dist/core/error-handler.js.map +1 -0
- package/dist/core/normalize-audit-result.d.ts +18 -0
- package/dist/core/normalize-audit-result.d.ts.map +1 -0
- package/dist/core/normalize-audit-result.js +118 -0
- package/dist/core/normalize-audit-result.js.map +1 -0
- package/dist/core/playwright-bootstrap.d.ts +21 -0
- package/dist/core/playwright-bootstrap.d.ts.map +1 -0
- package/dist/core/playwright-bootstrap.js +144 -0
- package/dist/core/playwright-bootstrap.js.map +1 -0
- package/dist/core/progress-streamer.d.ts +44 -0
- package/dist/core/progress-streamer.d.ts.map +1 -0
- package/dist/core/progress-streamer.js +160 -0
- package/dist/core/progress-streamer.js.map +1 -0
- package/dist/core/result-processor.d.ts +86 -0
- package/dist/core/result-processor.d.ts.map +1 -0
- package/dist/core/result-processor.js +475 -0
- package/dist/core/result-processor.js.map +1 -0
- package/dist/core/session-manager.d.ts +73 -0
- package/dist/core/session-manager.d.ts.map +1 -0
- package/dist/core/session-manager.js +243 -0
- package/dist/core/session-manager.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1439 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/aggregate.d.ts +26 -0
- package/dist/tools/aggregate.d.ts.map +1 -0
- package/dist/tools/aggregate.js +340 -0
- package/dist/tools/aggregate.js.map +1 -0
- package/dist/tools/analysis.d.ts +68 -0
- package/dist/tools/analysis.d.ts.map +1 -0
- package/dist/tools/analysis.js +1199 -0
- package/dist/tools/analysis.js.map +1 -0
- package/dist/tools/audit.d.ts +38 -0
- package/dist/tools/audit.d.ts.map +1 -0
- package/dist/tools/audit.js +472 -0
- package/dist/tools/audit.js.map +1 -0
- package/dist/tools/comparison.d.ts +27 -0
- package/dist/tools/comparison.d.ts.map +1 -0
- package/dist/tools/comparison.js +499 -0
- package/dist/tools/comparison.js.map +1 -0
- package/dist/tools/export.d.ts +43 -0
- package/dist/tools/export.d.ts.map +1 -0
- package/dist/tools/export.js +746 -0
- package/dist/tools/export.js.map +1 -0
- package/dist/tools/filter.d.ts +26 -0
- package/dist/tools/filter.d.ts.map +1 -0
- package/dist/tools/filter.js +244 -0
- package/dist/tools/filter.js.map +1 -0
- package/dist/tools/session.d.ts +26 -0
- package/dist/tools/session.d.ts.map +1 -0
- package/dist/tools/session.js +228 -0
- package/dist/tools/session.js.map +1 -0
- package/dist/tools/visualize.d.ts +26 -0
- package/dist/tools/visualize.d.ts.map +1 -0
- package/dist/tools/visualize.js +942 -0
- package/dist/tools/visualize.js.map +1 -0
- package/dist/types/index.d.ts +792 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +24 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export tools for audit results
|
|
3
|
+
* Implements: export_to_csv, export_to_excel, export_to_json, export_to_html_report
|
|
4
|
+
*/
|
|
5
|
+
import { resolveBasicAuth } from '../core/basic-auth.js';
|
|
6
|
+
import { normalizeAuditResult } from '../core/normalize-audit-result.js';
|
|
7
|
+
import { auditUrl } from './audit.js';
|
|
8
|
+
/**
|
|
9
|
+
* Escape CSV field - handles quotes, commas, and newlines
|
|
10
|
+
*/
|
|
11
|
+
function escapeCSV(field) {
|
|
12
|
+
if (field === null || field === undefined) {
|
|
13
|
+
return '""';
|
|
14
|
+
}
|
|
15
|
+
const stringField = String(field);
|
|
16
|
+
// If the field contains quotes, commas, or newlines, wrap it in quotes and escape internal quotes
|
|
17
|
+
if (stringField.includes('"') ||
|
|
18
|
+
stringField.includes(',') ||
|
|
19
|
+
stringField.includes('\n')) {
|
|
20
|
+
return `"${stringField.replace(/"/g, '""')}"`;
|
|
21
|
+
}
|
|
22
|
+
return stringField;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Generate metadata section for CSV
|
|
26
|
+
*/
|
|
27
|
+
function generateMetadataSection(result) {
|
|
28
|
+
const metadata = result.metadata;
|
|
29
|
+
if (!metadata) {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
const orientationInfo = metadata.testEnvironment.orientationAngle !== undefined
|
|
33
|
+
? `${metadata.testEnvironment.orientationType} (${metadata.testEnvironment.orientationAngle}°)`
|
|
34
|
+
: metadata.testEnvironment.orientationType;
|
|
35
|
+
return [
|
|
36
|
+
'Test Information',
|
|
37
|
+
`Test Engine,${escapeCSV(`${metadata.testEngine.name} v${metadata.testEngine.version}`)}`,
|
|
38
|
+
`Test Runner,${escapeCSV(metadata.testRunner.name)}`,
|
|
39
|
+
`Test URL,${escapeCSV(metadata.url)}`,
|
|
40
|
+
`Timestamp,${escapeCSV(metadata.timestamp)}`,
|
|
41
|
+
'',
|
|
42
|
+
'Environment Information',
|
|
43
|
+
`User Agent,${escapeCSV(metadata.testEnvironment.userAgent)}`,
|
|
44
|
+
`Window Size,${escapeCSV(`${metadata.testEnvironment.windowWidth}x${metadata.testEnvironment.windowHeight}`)}`,
|
|
45
|
+
`Orientation,${escapeCSV(orientationInfo || 'N/A')}`,
|
|
46
|
+
'',
|
|
47
|
+
'Summary',
|
|
48
|
+
`Total Issues,${result.summary.totalIssues}`,
|
|
49
|
+
`Accessibility Score,${result.summary.score}`,
|
|
50
|
+
`WCAG Level A Compliance,${result.summary.wcagCompliance.A}%`,
|
|
51
|
+
`WCAG Level AA Compliance,${result.summary.wcagCompliance.AA}%`,
|
|
52
|
+
`WCAG Level AAA Compliance,${result.summary.wcagCompliance.AAA}%`,
|
|
53
|
+
'',
|
|
54
|
+
'Test Results',
|
|
55
|
+
'',
|
|
56
|
+
].join('\n');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* export_to_csv - Export audit results to CSV format
|
|
60
|
+
*
|
|
61
|
+
* Exports audit results to CSV format for spreadsheet analysis, including
|
|
62
|
+
* metadata section and detailed violation rows.
|
|
63
|
+
*
|
|
64
|
+
* @param input - Export configuration
|
|
65
|
+
* @returns CSV content as string
|
|
66
|
+
*/
|
|
67
|
+
export async function exportToCsv(input) {
|
|
68
|
+
const { results, includeMetadata = true, includeViolations = true, format = 'standard', basicAuthUsername, basicAuthPassword, } = input;
|
|
69
|
+
let auditResult;
|
|
70
|
+
// If input is a URL string, run an audit first (with optional Basic Auth, same as audit_url)
|
|
71
|
+
if (typeof results === 'string') {
|
|
72
|
+
const { urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p } = resolveBasicAuth(results, basicAuthUsername, basicAuthPassword);
|
|
73
|
+
auditResult = await auditUrl({ url: urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p });
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const normalized = normalizeAuditResult(results);
|
|
77
|
+
auditResult = normalized ?? results;
|
|
78
|
+
}
|
|
79
|
+
const issues = Array.isArray(auditResult?.prioritizedIssues) ? auditResult.prioritizedIssues : [];
|
|
80
|
+
const csvRows = [];
|
|
81
|
+
// Add metadata section if requested
|
|
82
|
+
if (includeMetadata) {
|
|
83
|
+
csvRows.push(generateMetadataSection(auditResult));
|
|
84
|
+
}
|
|
85
|
+
// Add violation rows if requested
|
|
86
|
+
if (includeViolations) {
|
|
87
|
+
// Determine headers based on format
|
|
88
|
+
let headers;
|
|
89
|
+
if (format === 'minimal') {
|
|
90
|
+
headers = ['Rule ID', 'Impact', 'Description', 'Element'];
|
|
91
|
+
}
|
|
92
|
+
else if (format === 'detailed') {
|
|
93
|
+
headers = [
|
|
94
|
+
'Rule ID',
|
|
95
|
+
'Category',
|
|
96
|
+
'Impact',
|
|
97
|
+
'WCAG Level',
|
|
98
|
+
'Description',
|
|
99
|
+
'Element',
|
|
100
|
+
'XPath',
|
|
101
|
+
'Class selector',
|
|
102
|
+
'User Impact',
|
|
103
|
+
'Fix Explanation',
|
|
104
|
+
'Current Code',
|
|
105
|
+
'Suggested Code',
|
|
106
|
+
'Priority',
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// standard format
|
|
111
|
+
headers = [
|
|
112
|
+
'Rule ID',
|
|
113
|
+
'Category',
|
|
114
|
+
'Impact',
|
|
115
|
+
'WCAG Level',
|
|
116
|
+
'Description',
|
|
117
|
+
'Element',
|
|
118
|
+
'XPath',
|
|
119
|
+
'Class selector',
|
|
120
|
+
'User Impact',
|
|
121
|
+
'Fix Explanation',
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
// Add header row
|
|
125
|
+
csvRows.push(headers.map(escapeCSV).join(','));
|
|
126
|
+
// Add violation rows
|
|
127
|
+
issues.forEach((issue) => {
|
|
128
|
+
const row = [];
|
|
129
|
+
if (format === 'minimal') {
|
|
130
|
+
row.push(issue.ruleId, issue.impact, issue.description, issue.element);
|
|
131
|
+
}
|
|
132
|
+
else if (format === 'detailed') {
|
|
133
|
+
row.push(issue.ruleId, issue.category || 'unknown', issue.impact, issue.wcagLevel, issue.description, issue.element, issue.xpath, issue.classSelector ?? '', issue.userImpact, issue.fix.explanation, issue.fix.current, issue.fix.suggested, String(issue.priority));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// standard format
|
|
137
|
+
row.push(issue.ruleId, issue.category || 'unknown', issue.impact, issue.wcagLevel, issue.description, issue.element, issue.xpath, issue.classSelector ?? '', issue.userImpact, issue.fix.explanation);
|
|
138
|
+
}
|
|
139
|
+
csvRows.push(row.map(escapeCSV).join(','));
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const csvContent = csvRows.join('\n');
|
|
143
|
+
return {
|
|
144
|
+
csv: csvContent,
|
|
145
|
+
format,
|
|
146
|
+
totalIssues: auditResult.summary.totalIssues,
|
|
147
|
+
includeMetadata,
|
|
148
|
+
includeViolations,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* export_to_excel - Export audit results to Excel/XLSX format
|
|
153
|
+
*
|
|
154
|
+
* Exports audit results to Excel format with formatting. Requires xlsx package.
|
|
155
|
+
*
|
|
156
|
+
* @param input - Export configuration
|
|
157
|
+
* @returns Excel file content (base64 encoded)
|
|
158
|
+
*/
|
|
159
|
+
export async function exportToExcel(input) {
|
|
160
|
+
const { results, includeCharts = false, formatting = true, basicAuthUsername, basicAuthPassword, } = input;
|
|
161
|
+
let auditResult;
|
|
162
|
+
// If input is a URL string, run an audit first (with optional Basic Auth, same as audit_url)
|
|
163
|
+
if (typeof results === 'string') {
|
|
164
|
+
const { urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p } = resolveBasicAuth(results, basicAuthUsername, basicAuthPassword);
|
|
165
|
+
auditResult = await auditUrl({ url: urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p });
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const normalized = normalizeAuditResult(results);
|
|
169
|
+
auditResult = normalized ?? results;
|
|
170
|
+
}
|
|
171
|
+
const issues = Array.isArray(auditResult?.prioritizedIssues) ? auditResult.prioritizedIssues : [];
|
|
172
|
+
// Try to import xlsx, but handle gracefully if not available
|
|
173
|
+
let XLSX;
|
|
174
|
+
try {
|
|
175
|
+
XLSX = await import('xlsx');
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
throw new Error('xlsx package is required for Excel export. Install it with: npm install xlsx');
|
|
179
|
+
}
|
|
180
|
+
// Create workbook
|
|
181
|
+
const workbook = XLSX.utils.book_new();
|
|
182
|
+
// Create summary sheet
|
|
183
|
+
const summaryData = [
|
|
184
|
+
['Accessibility Audit Summary'],
|
|
185
|
+
[],
|
|
186
|
+
['Total Issues', auditResult.summary.totalIssues],
|
|
187
|
+
['Accessibility Score', auditResult.summary.score],
|
|
188
|
+
['WCAG Level A Compliance', `${auditResult.summary.wcagCompliance.A}%`],
|
|
189
|
+
['WCAG Level AA Compliance', `${auditResult.summary.wcagCompliance.AA}%`],
|
|
190
|
+
['WCAG Level AAA Compliance', `${auditResult.summary.wcagCompliance.AAA}%`],
|
|
191
|
+
[],
|
|
192
|
+
['Issues by Category'],
|
|
193
|
+
];
|
|
194
|
+
// Add category breakdown
|
|
195
|
+
Object.entries(auditResult.summary.byCategory).forEach(([category, count]) => {
|
|
196
|
+
summaryData.push([category, count]);
|
|
197
|
+
});
|
|
198
|
+
summaryData.push([]);
|
|
199
|
+
summaryData.push(['Issues by Impact']);
|
|
200
|
+
// Add impact breakdown
|
|
201
|
+
Object.entries(auditResult.summary.byImpact).forEach(([impact, count]) => {
|
|
202
|
+
summaryData.push([impact, count]);
|
|
203
|
+
});
|
|
204
|
+
// Add metadata if available
|
|
205
|
+
if (auditResult.metadata) {
|
|
206
|
+
summaryData.push([]);
|
|
207
|
+
summaryData.push(['Test Information']);
|
|
208
|
+
summaryData.push(['Test Engine', `${auditResult.metadata.testEngine.name} v${auditResult.metadata.testEngine.version}`]);
|
|
209
|
+
summaryData.push(['Test Runner', auditResult.metadata.testRunner.name]);
|
|
210
|
+
summaryData.push(['Test URL', auditResult.metadata.url]);
|
|
211
|
+
summaryData.push(['Timestamp', auditResult.metadata.timestamp]);
|
|
212
|
+
}
|
|
213
|
+
const summarySheet = XLSX.utils.aoa_to_sheet(summaryData);
|
|
214
|
+
if (formatting) {
|
|
215
|
+
// Set column widths
|
|
216
|
+
summarySheet['!cols'] = [{ wch: 30 }, { wch: 20 }];
|
|
217
|
+
}
|
|
218
|
+
XLSX.utils.book_append_sheet(workbook, summarySheet, 'Summary');
|
|
219
|
+
// Create violations sheet (WCAG Level = full label e.g. "WCAG 2.2 AA"; Element = tag name; Class selector after XPath)
|
|
220
|
+
const violationsData = [
|
|
221
|
+
[
|
|
222
|
+
'Rule ID',
|
|
223
|
+
'Category',
|
|
224
|
+
'Impact',
|
|
225
|
+
'WCAG Level',
|
|
226
|
+
'Description',
|
|
227
|
+
'Element',
|
|
228
|
+
'XPath',
|
|
229
|
+
'Class selector',
|
|
230
|
+
'User Impact',
|
|
231
|
+
'Fix Explanation',
|
|
232
|
+
],
|
|
233
|
+
];
|
|
234
|
+
issues.forEach((issue) => {
|
|
235
|
+
violationsData.push([
|
|
236
|
+
issue.ruleId,
|
|
237
|
+
issue.category || 'unknown',
|
|
238
|
+
issue.impact,
|
|
239
|
+
issue.wcagLevel,
|
|
240
|
+
issue.description,
|
|
241
|
+
issue.element,
|
|
242
|
+
issue.xpath,
|
|
243
|
+
issue.classSelector ?? '',
|
|
244
|
+
issue.userImpact,
|
|
245
|
+
issue.fix.explanation,
|
|
246
|
+
]);
|
|
247
|
+
});
|
|
248
|
+
const violationsSheet = XLSX.utils.aoa_to_sheet(violationsData);
|
|
249
|
+
if (formatting) {
|
|
250
|
+
// Set column widths
|
|
251
|
+
violationsSheet['!cols'] = [
|
|
252
|
+
{ wch: 20 }, // Rule ID
|
|
253
|
+
{ wch: 15 }, // Category
|
|
254
|
+
{ wch: 12 }, // Impact
|
|
255
|
+
{ wch: 14 }, // WCAG Level
|
|
256
|
+
{ wch: 40 }, // Description
|
|
257
|
+
{ wch: 30 }, // Element
|
|
258
|
+
{ wch: 50 }, // XPath
|
|
259
|
+
{ wch: 28 }, // Class selector
|
|
260
|
+
{ wch: 50 }, // User Impact
|
|
261
|
+
{ wch: 50 }, // Fix Explanation
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
XLSX.utils.book_append_sheet(workbook, violationsSheet, 'Violations');
|
|
265
|
+
// Generate Excel file as base64
|
|
266
|
+
const excelBuffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
|
267
|
+
const base64Content = excelBuffer.toString('base64');
|
|
268
|
+
return {
|
|
269
|
+
excel: base64Content,
|
|
270
|
+
format: 'xlsx',
|
|
271
|
+
totalIssues: auditResult.summary.totalIssues,
|
|
272
|
+
includeCharts,
|
|
273
|
+
formatting,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* export_to_json - Export audit results as structured JSON
|
|
278
|
+
*
|
|
279
|
+
* Exports audit results as structured JSON with optional raw results.
|
|
280
|
+
*
|
|
281
|
+
* @param input - Export configuration
|
|
282
|
+
* @returns JSON string
|
|
283
|
+
*/
|
|
284
|
+
export async function exportToJson(input) {
|
|
285
|
+
const { results, pretty = true, includeRaw = false, basicAuthUsername, basicAuthPassword, } = input;
|
|
286
|
+
let auditResult;
|
|
287
|
+
// If input is a URL string, run an audit first (with optional Basic Auth, same as audit_url)
|
|
288
|
+
if (typeof results === 'string') {
|
|
289
|
+
const { urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p } = resolveBasicAuth(results, basicAuthUsername, basicAuthPassword);
|
|
290
|
+
auditResult = await auditUrl({ url: urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p });
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
const normalized = normalizeAuditResult(results);
|
|
294
|
+
auditResult = normalized ?? results;
|
|
295
|
+
}
|
|
296
|
+
// Prepare export data
|
|
297
|
+
const exportData = {
|
|
298
|
+
summary: auditResult.summary,
|
|
299
|
+
prioritizedIssues: auditResult.prioritizedIssues,
|
|
300
|
+
quickWins: auditResult.quickWins,
|
|
301
|
+
criticalBlockers: auditResult.criticalBlockers,
|
|
302
|
+
conversationalSummary: auditResult.conversationalSummary,
|
|
303
|
+
metadata: auditResult.metadata,
|
|
304
|
+
};
|
|
305
|
+
// Include raw results if requested
|
|
306
|
+
if (includeRaw && auditResult.rawResults) {
|
|
307
|
+
exportData.rawResults = auditResult.rawResults;
|
|
308
|
+
}
|
|
309
|
+
// Generate JSON
|
|
310
|
+
const jsonContent = pretty
|
|
311
|
+
? JSON.stringify(exportData, null, 2)
|
|
312
|
+
: JSON.stringify(exportData);
|
|
313
|
+
return {
|
|
314
|
+
json: jsonContent,
|
|
315
|
+
pretty,
|
|
316
|
+
includeRaw,
|
|
317
|
+
totalIssues: auditResult.summary.totalIssues,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Escape HTML special characters
|
|
322
|
+
*/
|
|
323
|
+
function escapeHtml(text) {
|
|
324
|
+
return text
|
|
325
|
+
.replace(/&/g, '&')
|
|
326
|
+
.replace(/</g, '<')
|
|
327
|
+
.replace(/>/g, '>')
|
|
328
|
+
.replace(/"/g, '"')
|
|
329
|
+
.replace(/'/g, ''');
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Generate HTML report template
|
|
333
|
+
*/
|
|
334
|
+
function generateHtmlReport(result, template, includeCharts) {
|
|
335
|
+
const metadata = result.metadata;
|
|
336
|
+
const summary = result.summary;
|
|
337
|
+
const issues = Array.isArray(result?.prioritizedIssues) ? result.prioritizedIssues : [];
|
|
338
|
+
// Generate chart data if requested
|
|
339
|
+
let chartScript = '';
|
|
340
|
+
if (includeCharts) {
|
|
341
|
+
chartScript = `
|
|
342
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
|
|
343
|
+
<script>
|
|
344
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
345
|
+
// Category breakdown chart
|
|
346
|
+
const categoryCtx = document.getElementById('categoryChart');
|
|
347
|
+
if (categoryCtx) {
|
|
348
|
+
new Chart(categoryCtx, {
|
|
349
|
+
type: 'bar',
|
|
350
|
+
data: {
|
|
351
|
+
labels: ${JSON.stringify(Object.keys(summary.byCategory))},
|
|
352
|
+
datasets: [{
|
|
353
|
+
label: 'Issues by Category',
|
|
354
|
+
data: ${JSON.stringify(Object.values(summary.byCategory))},
|
|
355
|
+
backgroundColor: 'rgba(54, 162, 235, 0.5)',
|
|
356
|
+
borderColor: 'rgba(54, 162, 235, 1)',
|
|
357
|
+
borderWidth: 1
|
|
358
|
+
}]
|
|
359
|
+
},
|
|
360
|
+
options: {
|
|
361
|
+
responsive: true,
|
|
362
|
+
scales: {
|
|
363
|
+
y: { beginAtZero: true }
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Impact breakdown chart
|
|
370
|
+
const impactCtx = document.getElementById('impactChart');
|
|
371
|
+
if (impactCtx) {
|
|
372
|
+
new Chart(impactCtx, {
|
|
373
|
+
type: 'doughnut',
|
|
374
|
+
data: {
|
|
375
|
+
labels: ${JSON.stringify(Object.keys(summary.byImpact))},
|
|
376
|
+
datasets: [{
|
|
377
|
+
data: ${JSON.stringify(Object.values(summary.byImpact))},
|
|
378
|
+
backgroundColor: [
|
|
379
|
+
'rgba(255, 99, 132, 0.5)',
|
|
380
|
+
'rgba(255, 159, 64, 0.5)',
|
|
381
|
+
'rgba(255, 205, 86, 0.5)',
|
|
382
|
+
'rgba(75, 192, 192, 0.5)'
|
|
383
|
+
]
|
|
384
|
+
}]
|
|
385
|
+
},
|
|
386
|
+
options: {
|
|
387
|
+
responsive: true
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
</script>
|
|
393
|
+
`;
|
|
394
|
+
}
|
|
395
|
+
// Generate issues table rows
|
|
396
|
+
const issuesRows = issues
|
|
397
|
+
.map((issue) => `
|
|
398
|
+
<tr>
|
|
399
|
+
<td>${escapeHtml(issue.ruleId)}</td>
|
|
400
|
+
<td>${escapeHtml(issue.category || 'unknown')}</td>
|
|
401
|
+
<td><span class="impact-${issue.impact}">${escapeHtml(issue.impact)}</span></td>
|
|
402
|
+
<td>${escapeHtml(issue.wcagLevel)}</td>
|
|
403
|
+
<td>${escapeHtml(issue.description)}</td>
|
|
404
|
+
<td><code>${escapeHtml(issue.element)}</code></td>
|
|
405
|
+
<td>${escapeHtml(issue.userImpact)}</td>
|
|
406
|
+
</tr>
|
|
407
|
+
`)
|
|
408
|
+
.join('');
|
|
409
|
+
// Template-specific content
|
|
410
|
+
let mainContent = '';
|
|
411
|
+
if (template === 'minimal') {
|
|
412
|
+
mainContent = `
|
|
413
|
+
<div class="summary">
|
|
414
|
+
<h2>Summary</h2>
|
|
415
|
+
<p>Total Issues: <strong>${summary.totalIssues}</strong></p>
|
|
416
|
+
<p>Accessibility Score: <strong>${summary.score}/100</strong></p>
|
|
417
|
+
</div>
|
|
418
|
+
<div class="issues">
|
|
419
|
+
<h2>Issues</h2>
|
|
420
|
+
<table>
|
|
421
|
+
<thead>
|
|
422
|
+
<tr>
|
|
423
|
+
<th>Rule ID</th>
|
|
424
|
+
<th>Impact</th>
|
|
425
|
+
<th>Description</th>
|
|
426
|
+
<th>Element</th>
|
|
427
|
+
</tr>
|
|
428
|
+
</thead>
|
|
429
|
+
<tbody>
|
|
430
|
+
${issues
|
|
431
|
+
.map((issue) => `
|
|
432
|
+
<tr>
|
|
433
|
+
<td>${escapeHtml(issue.ruleId)}</td>
|
|
434
|
+
<td><span class="impact-${issue.impact}">${escapeHtml(issue.impact)}</span></td>
|
|
435
|
+
<td>${escapeHtml(issue.description)}</td>
|
|
436
|
+
<td><code>${escapeHtml(issue.element)}</code></td>
|
|
437
|
+
</tr>
|
|
438
|
+
`)
|
|
439
|
+
.join('')}
|
|
440
|
+
</tbody>
|
|
441
|
+
</table>
|
|
442
|
+
</div>
|
|
443
|
+
`;
|
|
444
|
+
}
|
|
445
|
+
else if (template === 'detailed') {
|
|
446
|
+
mainContent = `
|
|
447
|
+
<div class="summary">
|
|
448
|
+
<h2>Summary</h2>
|
|
449
|
+
<p>Total Issues: <strong>${summary.totalIssues}</strong></p>
|
|
450
|
+
<p>Accessibility Score: <strong>${summary.score}/100</strong></p>
|
|
451
|
+
<p>WCAG Level A Compliance: <strong>${summary.wcagCompliance.A}%</strong></p>
|
|
452
|
+
<p>WCAG Level AA Compliance: <strong>${summary.wcagCompliance.AA}%</strong></p>
|
|
453
|
+
<p>WCAG Level AAA Compliance: <strong>${summary.wcagCompliance.AAA}%</strong></p>
|
|
454
|
+
</div>
|
|
455
|
+
${includeCharts ? `
|
|
456
|
+
<div class="charts">
|
|
457
|
+
<div style="width: 50%; display: inline-block;">
|
|
458
|
+
<canvas id="categoryChart"></canvas>
|
|
459
|
+
</div>
|
|
460
|
+
<div style="width: 50%; display: inline-block;">
|
|
461
|
+
<canvas id="impactChart"></canvas>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
` : ''}
|
|
465
|
+
<div class="issues">
|
|
466
|
+
<h2>Detailed Issues</h2>
|
|
467
|
+
<table>
|
|
468
|
+
<thead>
|
|
469
|
+
<tr>
|
|
470
|
+
<th>Rule ID</th>
|
|
471
|
+
<th>Category</th>
|
|
472
|
+
<th>Impact</th>
|
|
473
|
+
<th>WCAG Level</th>
|
|
474
|
+
<th>Description</th>
|
|
475
|
+
<th>Element</th>
|
|
476
|
+
<th>XPath</th>
|
|
477
|
+
<th>Class selector</th>
|
|
478
|
+
<th>User Impact</th>
|
|
479
|
+
<th>Fix Explanation</th>
|
|
480
|
+
<th>Current Code</th>
|
|
481
|
+
<th>Suggested Code</th>
|
|
482
|
+
</tr>
|
|
483
|
+
</thead>
|
|
484
|
+
<tbody>
|
|
485
|
+
${issues
|
|
486
|
+
.map((issue) => `
|
|
487
|
+
<tr>
|
|
488
|
+
<td>${escapeHtml(issue.ruleId)}</td>
|
|
489
|
+
<td>${escapeHtml(issue.category || 'unknown')}</td>
|
|
490
|
+
<td><span class="impact-${issue.impact}">${escapeHtml(issue.impact)}</span></td>
|
|
491
|
+
<td>${escapeHtml(issue.wcagLevel)}</td>
|
|
492
|
+
<td>${escapeHtml(issue.description)}</td>
|
|
493
|
+
<td><code>${escapeHtml(issue.element)}</code></td>
|
|
494
|
+
<td><code>${escapeHtml(issue.xpath)}</code></td>
|
|
495
|
+
<td><code>${escapeHtml(issue.classSelector ?? '')}</code></td>
|
|
496
|
+
<td>${escapeHtml(issue.userImpact)}</td>
|
|
497
|
+
<td>${escapeHtml(issue.fix.explanation)}</td>
|
|
498
|
+
<td><pre>${escapeHtml(issue.fix.current)}</pre></td>
|
|
499
|
+
<td><pre>${escapeHtml(issue.fix.suggested)}</pre></td>
|
|
500
|
+
</tr>
|
|
501
|
+
`)
|
|
502
|
+
.join('')}
|
|
503
|
+
</tbody>
|
|
504
|
+
</table>
|
|
505
|
+
</div>
|
|
506
|
+
`;
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
// default template
|
|
510
|
+
mainContent = `
|
|
511
|
+
<div class="summary">
|
|
512
|
+
<h2>Summary</h2>
|
|
513
|
+
<p>Total Issues: <strong>${summary.totalIssues}</strong></p>
|
|
514
|
+
<p>Accessibility Score: <strong>${summary.score}/100</strong></p>
|
|
515
|
+
<p>WCAG Level A Compliance: <strong>${summary.wcagCompliance.A}%</strong></p>
|
|
516
|
+
<p>WCAG Level AA Compliance: <strong>${summary.wcagCompliance.AA}%</strong></p>
|
|
517
|
+
<p>WCAG Level AAA Compliance: <strong>${summary.wcagCompliance.AAA}%</strong></p>
|
|
518
|
+
</div>
|
|
519
|
+
${includeCharts ? `
|
|
520
|
+
<div class="charts">
|
|
521
|
+
<div style="width: 50%; display: inline-block;">
|
|
522
|
+
<canvas id="categoryChart"></canvas>
|
|
523
|
+
</div>
|
|
524
|
+
<div style="width: 50%; display: inline-block;">
|
|
525
|
+
<canvas id="impactChart"></canvas>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
` : ''}
|
|
529
|
+
<div class="issues">
|
|
530
|
+
<h2>Issues</h2>
|
|
531
|
+
<table>
|
|
532
|
+
<thead>
|
|
533
|
+
<tr>
|
|
534
|
+
<th>Rule ID</th>
|
|
535
|
+
<th>Category</th>
|
|
536
|
+
<th>Impact</th>
|
|
537
|
+
<th>WCAG Level</th>
|
|
538
|
+
<th>Description</th>
|
|
539
|
+
<th>Element</th>
|
|
540
|
+
<th>User Impact</th>
|
|
541
|
+
</tr>
|
|
542
|
+
</thead>
|
|
543
|
+
<tbody>
|
|
544
|
+
${issuesRows}
|
|
545
|
+
</tbody>
|
|
546
|
+
</table>
|
|
547
|
+
</div>
|
|
548
|
+
`;
|
|
549
|
+
}
|
|
550
|
+
return `
|
|
551
|
+
<!DOCTYPE html>
|
|
552
|
+
<html lang="en">
|
|
553
|
+
<head>
|
|
554
|
+
<meta charset="UTF-8">
|
|
555
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
556
|
+
<title>Accessibility Audit Report</title>
|
|
557
|
+
<style>
|
|
558
|
+
body {
|
|
559
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
560
|
+
line-height: 1.6;
|
|
561
|
+
color: #333;
|
|
562
|
+
max-width: 1200px;
|
|
563
|
+
margin: 0 auto;
|
|
564
|
+
padding: 20px;
|
|
565
|
+
background-color: #f5f5f5;
|
|
566
|
+
}
|
|
567
|
+
.header {
|
|
568
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
569
|
+
color: white;
|
|
570
|
+
padding: 30px;
|
|
571
|
+
border-radius: 8px;
|
|
572
|
+
margin-bottom: 30px;
|
|
573
|
+
}
|
|
574
|
+
.header h1 {
|
|
575
|
+
margin: 0;
|
|
576
|
+
font-size: 2em;
|
|
577
|
+
}
|
|
578
|
+
.metadata {
|
|
579
|
+
background: white;
|
|
580
|
+
padding: 20px;
|
|
581
|
+
border-radius: 8px;
|
|
582
|
+
margin-bottom: 20px;
|
|
583
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
584
|
+
}
|
|
585
|
+
.metadata h2 {
|
|
586
|
+
margin-top: 0;
|
|
587
|
+
color: #667eea;
|
|
588
|
+
}
|
|
589
|
+
.metadata p {
|
|
590
|
+
margin: 5px 0;
|
|
591
|
+
}
|
|
592
|
+
.summary {
|
|
593
|
+
background: white;
|
|
594
|
+
padding: 20px;
|
|
595
|
+
border-radius: 8px;
|
|
596
|
+
margin-bottom: 20px;
|
|
597
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
598
|
+
}
|
|
599
|
+
.summary h2 {
|
|
600
|
+
margin-top: 0;
|
|
601
|
+
color: #667eea;
|
|
602
|
+
}
|
|
603
|
+
.summary p {
|
|
604
|
+
margin: 10px 0;
|
|
605
|
+
font-size: 1.1em;
|
|
606
|
+
}
|
|
607
|
+
.charts {
|
|
608
|
+
background: white;
|
|
609
|
+
padding: 20px;
|
|
610
|
+
border-radius: 8px;
|
|
611
|
+
margin-bottom: 20px;
|
|
612
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
613
|
+
}
|
|
614
|
+
.issues {
|
|
615
|
+
background: white;
|
|
616
|
+
padding: 20px;
|
|
617
|
+
border-radius: 8px;
|
|
618
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
619
|
+
}
|
|
620
|
+
.issues h2 {
|
|
621
|
+
margin-top: 0;
|
|
622
|
+
color: #667eea;
|
|
623
|
+
}
|
|
624
|
+
table {
|
|
625
|
+
width: 100%;
|
|
626
|
+
border-collapse: collapse;
|
|
627
|
+
margin-top: 20px;
|
|
628
|
+
}
|
|
629
|
+
th {
|
|
630
|
+
background-color: #667eea;
|
|
631
|
+
color: white;
|
|
632
|
+
padding: 12px;
|
|
633
|
+
text-align: left;
|
|
634
|
+
font-weight: 600;
|
|
635
|
+
}
|
|
636
|
+
td {
|
|
637
|
+
padding: 10px;
|
|
638
|
+
border-bottom: 1px solid #ddd;
|
|
639
|
+
}
|
|
640
|
+
tr:hover {
|
|
641
|
+
background-color: #f9f9f9;
|
|
642
|
+
}
|
|
643
|
+
code {
|
|
644
|
+
background-color: #f4f4f4;
|
|
645
|
+
padding: 2px 6px;
|
|
646
|
+
border-radius: 3px;
|
|
647
|
+
font-family: 'Courier New', monospace;
|
|
648
|
+
font-size: 0.9em;
|
|
649
|
+
}
|
|
650
|
+
pre {
|
|
651
|
+
background-color: #f4f4f4;
|
|
652
|
+
padding: 10px;
|
|
653
|
+
border-radius: 3px;
|
|
654
|
+
overflow-x: auto;
|
|
655
|
+
font-size: 0.85em;
|
|
656
|
+
}
|
|
657
|
+
/* axe: critical, serious, moderate, minor. ACE: violation, potentialviolation, etc. */
|
|
658
|
+
.impact-critical, .impact-violation {
|
|
659
|
+
background-color: #ff4444;
|
|
660
|
+
color: white;
|
|
661
|
+
padding: 4px 8px;
|
|
662
|
+
border-radius: 4px;
|
|
663
|
+
font-weight: bold;
|
|
664
|
+
font-size: 0.85em;
|
|
665
|
+
}
|
|
666
|
+
.impact-serious, .impact-potentialviolation {
|
|
667
|
+
background-color: #ff8800;
|
|
668
|
+
color: white;
|
|
669
|
+
padding: 4px 8px;
|
|
670
|
+
border-radius: 4px;
|
|
671
|
+
font-weight: bold;
|
|
672
|
+
font-size: 0.85em;
|
|
673
|
+
}
|
|
674
|
+
.impact-moderate, .impact-potentialrecommendation, .impact-manual {
|
|
675
|
+
background-color: #ffbb00;
|
|
676
|
+
color: white;
|
|
677
|
+
padding: 4px 8px;
|
|
678
|
+
border-radius: 4px;
|
|
679
|
+
font-weight: bold;
|
|
680
|
+
font-size: 0.85em;
|
|
681
|
+
}
|
|
682
|
+
.impact-minor, .impact-recommendation, .impact-pass {
|
|
683
|
+
background-color: #88cc00;
|
|
684
|
+
color: white;
|
|
685
|
+
padding: 4px 8px;
|
|
686
|
+
border-radius: 4px;
|
|
687
|
+
font-weight: bold;
|
|
688
|
+
font-size: 0.85em;
|
|
689
|
+
}
|
|
690
|
+
[class^="impact-"] {
|
|
691
|
+
padding: 4px 8px;
|
|
692
|
+
border-radius: 4px;
|
|
693
|
+
font-size: 0.85em;
|
|
694
|
+
}
|
|
695
|
+
</style>
|
|
696
|
+
${chartScript}
|
|
697
|
+
</head>
|
|
698
|
+
<body>
|
|
699
|
+
<div class="header">
|
|
700
|
+
<h1>Accessibility Audit Report</h1>
|
|
701
|
+
</div>
|
|
702
|
+
${metadata ? `
|
|
703
|
+
<div class="metadata">
|
|
704
|
+
<h2>Test Information</h2>
|
|
705
|
+
<p><strong>Test Engine:</strong> ${metadata.testEngine.name} v${metadata.testEngine.version}</p>
|
|
706
|
+
<p><strong>Test Runner:</strong> ${metadata.testRunner.name}</p>
|
|
707
|
+
<p><strong>Test URL:</strong> <a href="${metadata.url}" target="_blank">${metadata.url}</a></p>
|
|
708
|
+
<p><strong>Timestamp:</strong> ${metadata.timestamp}</p>
|
|
709
|
+
<p><strong>User Agent:</strong> ${metadata.testEnvironment.userAgent}</p>
|
|
710
|
+
<p><strong>Window Size:</strong> ${metadata.testEnvironment.windowWidth}x${metadata.testEnvironment.windowHeight}</p>
|
|
711
|
+
</div>
|
|
712
|
+
` : ''}
|
|
713
|
+
${mainContent}
|
|
714
|
+
</body>
|
|
715
|
+
</html>
|
|
716
|
+
`;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* export_to_html_report - Generate standalone HTML report
|
|
720
|
+
*
|
|
721
|
+
* Generates a standalone HTML report with styling and optional charts.
|
|
722
|
+
*
|
|
723
|
+
* @param input - Export configuration
|
|
724
|
+
* @returns HTML string with embedded CSS/JS
|
|
725
|
+
*/
|
|
726
|
+
export async function exportToHtmlReport(input) {
|
|
727
|
+
const { results, template = 'default', includeCharts = true, basicAuthUsername, basicAuthPassword, } = input;
|
|
728
|
+
let auditResult;
|
|
729
|
+
// If input is a URL string, run an audit first (with optional Basic Auth, same as audit_url)
|
|
730
|
+
if (typeof results === 'string') {
|
|
731
|
+
const { urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p } = resolveBasicAuth(results, basicAuthUsername, basicAuthPassword);
|
|
732
|
+
auditResult = await auditUrl({ url: urlWithoutAuth, basicAuthUsername: u, basicAuthPassword: p });
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
const normalized = normalizeAuditResult(results);
|
|
736
|
+
auditResult = normalized ?? results;
|
|
737
|
+
}
|
|
738
|
+
const htmlContent = generateHtmlReport(auditResult, template, includeCharts);
|
|
739
|
+
return {
|
|
740
|
+
html: htmlContent,
|
|
741
|
+
template,
|
|
742
|
+
includeCharts,
|
|
743
|
+
totalIssues: auditResult.summary.totalIssues,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
//# sourceMappingURL=export.js.map
|