pulse-js-framework 1.7.9 → 1.7.10

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.
@@ -0,0 +1,442 @@
1
+ /**
2
+ * Pulse DevTools - Accessibility Audit Module
3
+ * @module pulse-js-framework/runtime/devtools/a11y-audit
4
+ *
5
+ * Real-time accessibility validation with visual highlighting and reports.
6
+ */
7
+
8
+ import { validateA11y, highlightA11yIssues } from '../a11y.js';
9
+ import { createLogger } from '../logger.js';
10
+ import { config as diagnosticsConfig } from './diagnostics.js';
11
+
12
+ const log = createLogger('DevTools:A11y');
13
+
14
+ // =============================================================================
15
+ // A11Y AUDIT CONFIGURATION
16
+ // =============================================================================
17
+
18
+ /**
19
+ * A11y audit configuration
20
+ */
21
+ export const a11yAuditConfig = {
22
+ enabled: false,
23
+ autoAudit: false,
24
+ auditInterval: 5000,
25
+ highlightIssues: true,
26
+ logToConsole: true,
27
+ breakOnError: false,
28
+ watchMutations: false
29
+ };
30
+
31
+ /**
32
+ * Current a11y audit state
33
+ */
34
+ let a11yAuditState = {
35
+ issues: [],
36
+ lastAuditTime: null,
37
+ auditCount: 0,
38
+ highlightCleanup: null,
39
+ mutationObserver: null,
40
+ intervalId: null,
41
+ mutationTimeout: null
42
+ };
43
+
44
+ // =============================================================================
45
+ // A11Y AUDIT API
46
+ // =============================================================================
47
+
48
+ /**
49
+ * @typedef {Object} A11yAuditResult
50
+ * @property {Array} issues - List of accessibility issues found
51
+ * @property {number} errorCount - Number of errors
52
+ * @property {number} warningCount - Number of warnings
53
+ * @property {number} auditTime - Time taken for audit in ms
54
+ * @property {string} timestamp - ISO timestamp of audit
55
+ */
56
+
57
+ /**
58
+ * Run an accessibility audit on the document or specific element
59
+ * @param {Element} [root=document.body] - Root element to audit
60
+ * @param {Object} [options] - Audit options
61
+ * @returns {A11yAuditResult} Audit result
62
+ */
63
+ export function runA11yAudit(root, options = {}) {
64
+ if (typeof document === 'undefined') {
65
+ return { issues: [], errorCount: 0, warningCount: 0, auditTime: 0, timestamp: new Date().toISOString() };
66
+ }
67
+
68
+ const startTime = performance.now();
69
+ const targetRoot = root || document.body;
70
+
71
+ // Run validation with options
72
+ const issues = validateA11y(targetRoot, options);
73
+
74
+ const auditTime = performance.now() - startTime;
75
+ a11yAuditState.lastAuditTime = Date.now();
76
+ a11yAuditState.auditCount++;
77
+ a11yAuditState.issues = issues;
78
+
79
+ // Count by severity
80
+ const errorCount = issues.filter(i => i.severity === 'error').length;
81
+ const warningCount = issues.filter(i => i.severity === 'warning').length;
82
+
83
+ const result = {
84
+ issues,
85
+ errorCount,
86
+ warningCount,
87
+ auditTime,
88
+ timestamp: new Date().toISOString()
89
+ };
90
+
91
+ // Log to console if enabled
92
+ if (a11yAuditConfig.logToConsole && diagnosticsConfig.enabled) {
93
+ logA11yAuditResult(result);
94
+ }
95
+
96
+ // Highlight issues if enabled
97
+ if (a11yAuditConfig.highlightIssues && diagnosticsConfig.enabled) {
98
+ if (a11yAuditState.highlightCleanup) {
99
+ a11yAuditState.highlightCleanup();
100
+ }
101
+ a11yAuditState.highlightCleanup = highlightA11yIssues(issues);
102
+ }
103
+
104
+ // Break on error if configured
105
+ if (a11yAuditConfig.breakOnError && errorCount > 0) {
106
+ log.error('Breaking due to accessibility errors');
107
+ // eslint-disable-next-line no-debugger
108
+ debugger;
109
+ }
110
+
111
+ return result;
112
+ }
113
+
114
+ /**
115
+ * Log audit result to console with formatting
116
+ * @private
117
+ */
118
+ function logA11yAuditResult(result) {
119
+ const { issues, errorCount, warningCount, auditTime } = result;
120
+
121
+ console.group(`%c[A11y Audit] ${errorCount} errors, ${warningCount} warnings (${auditTime.toFixed(1)}ms)`,
122
+ errorCount > 0 ? 'color: red; font-weight: bold' : 'color: green; font-weight: bold');
123
+
124
+ if (issues.length === 0) {
125
+ console.log('%c✓ No accessibility issues found', 'color: green');
126
+ } else {
127
+ // Group by severity
128
+ const errors = issues.filter(i => i.severity === 'error');
129
+ const warnings = issues.filter(i => i.severity === 'warning');
130
+
131
+ if (errors.length > 0) {
132
+ console.group('%cErrors', 'color: red; font-weight: bold');
133
+ for (const issue of errors) {
134
+ console.error(`${issue.rule}: ${issue.message}`, issue.element || '');
135
+ }
136
+ console.groupEnd();
137
+ }
138
+
139
+ if (warnings.length > 0) {
140
+ console.group('%cWarnings', 'color: orange');
141
+ for (const issue of warnings) {
142
+ console.warn(`${issue.rule}: ${issue.message}`, issue.element || '');
143
+ }
144
+ console.groupEnd();
145
+ }
146
+ }
147
+
148
+ console.groupEnd();
149
+ }
150
+
151
+ /**
152
+ * Get current accessibility issues from last audit
153
+ * @returns {Array} List of a11y issues
154
+ */
155
+ export function getA11yIssues() {
156
+ return [...a11yAuditState.issues];
157
+ }
158
+
159
+ /**
160
+ * Get a11y audit statistics
161
+ * @returns {Object} Audit statistics
162
+ */
163
+ export function getA11yStats() {
164
+ const issues = a11yAuditState.issues;
165
+ const byRule = {};
166
+
167
+ for (const issue of issues) {
168
+ byRule[issue.rule] = (byRule[issue.rule] || 0) + 1;
169
+ }
170
+
171
+ return {
172
+ totalIssues: issues.length,
173
+ errorCount: issues.filter(i => i.severity === 'error').length,
174
+ warningCount: issues.filter(i => i.severity === 'warning').length,
175
+ issuesByRule: byRule,
176
+ auditCount: a11yAuditState.auditCount,
177
+ lastAuditTime: a11yAuditState.lastAuditTime,
178
+ isWatching: a11yAuditState.mutationObserver !== null,
179
+ isAutoAuditing: a11yAuditState.intervalId !== null
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Enable accessibility audit mode
185
+ * @param {Object} [options] - Configuration options
186
+ */
187
+ export function enableA11yAudit(options = {}) {
188
+ Object.assign(a11yAuditConfig, options, { enabled: true });
189
+
190
+ // Run initial audit
191
+ runA11yAudit();
192
+
193
+ // Setup auto-audit if enabled
194
+ if (a11yAuditConfig.autoAudit && typeof window !== 'undefined') {
195
+ a11yAuditState.intervalId = setInterval(() => {
196
+ runA11yAudit();
197
+ }, a11yAuditConfig.auditInterval);
198
+ }
199
+
200
+ // Setup mutation observer if enabled
201
+ if (a11yAuditConfig.watchMutations && typeof MutationObserver !== 'undefined') {
202
+ a11yAuditState.mutationObserver = new MutationObserver(() => {
203
+ // Debounce audit on mutations
204
+ clearTimeout(a11yAuditState.mutationTimeout);
205
+ a11yAuditState.mutationTimeout = setTimeout(() => {
206
+ runA11yAudit();
207
+ }, 250);
208
+ });
209
+
210
+ a11yAuditState.mutationObserver.observe(document.body, {
211
+ childList: true,
212
+ subtree: true,
213
+ attributes: true,
214
+ attributeFilter: ['role', 'aria-label', 'aria-hidden', 'aria-describedby', 'alt', 'tabindex']
215
+ });
216
+ }
217
+
218
+ if (diagnosticsConfig.enabled) {
219
+ log.info('A11y Audit enabled', a11yAuditConfig);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Disable accessibility audit mode
225
+ */
226
+ export function disableA11yAudit() {
227
+ a11yAuditConfig.enabled = false;
228
+
229
+ // Clear auto-audit interval
230
+ if (a11yAuditState.intervalId) {
231
+ clearInterval(a11yAuditState.intervalId);
232
+ a11yAuditState.intervalId = null;
233
+ }
234
+
235
+ // Disconnect mutation observer
236
+ if (a11yAuditState.mutationObserver) {
237
+ a11yAuditState.mutationObserver.disconnect();
238
+ a11yAuditState.mutationObserver = null;
239
+ }
240
+
241
+ // Clear highlights
242
+ if (a11yAuditState.highlightCleanup) {
243
+ a11yAuditState.highlightCleanup();
244
+ a11yAuditState.highlightCleanup = null;
245
+ }
246
+
247
+ if (diagnosticsConfig.enabled) {
248
+ log.info('A11y Audit disabled');
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Toggle issue highlighting
254
+ * @param {boolean} [show] - Show or hide highlights (toggles if not specified)
255
+ */
256
+ export function toggleA11yHighlights(show) {
257
+ const shouldShow = show !== undefined ? show : !a11yAuditState.highlightCleanup;
258
+
259
+ if (shouldShow) {
260
+ if (a11yAuditState.highlightCleanup) {
261
+ a11yAuditState.highlightCleanup();
262
+ }
263
+ a11yAuditState.highlightCleanup = highlightA11yIssues(a11yAuditState.issues);
264
+ } else {
265
+ if (a11yAuditState.highlightCleanup) {
266
+ a11yAuditState.highlightCleanup();
267
+ a11yAuditState.highlightCleanup = null;
268
+ }
269
+ }
270
+ }
271
+
272
+ // =============================================================================
273
+ // EXPORT REPORTS
274
+ // =============================================================================
275
+
276
+ /**
277
+ * Get a CSS selector for an element
278
+ * @private
279
+ */
280
+ function getElementSelector(element) {
281
+ if (!element) return 'unknown';
282
+
283
+ const parts = [];
284
+ let el = element;
285
+
286
+ while (el && el !== document.body) {
287
+ let selector = el.tagName?.toLowerCase() || '';
288
+
289
+ if (el.id) {
290
+ selector += `#${el.id}`;
291
+ parts.unshift(selector);
292
+ break;
293
+ }
294
+
295
+ if (el.className && typeof el.className === 'string') {
296
+ const classes = el.className.trim().split(/\s+/).slice(0, 2).join('.');
297
+ if (classes) selector += `.${classes}`;
298
+ }
299
+
300
+ parts.unshift(selector);
301
+ el = el.parentElement;
302
+ }
303
+
304
+ return parts.join(' > ');
305
+ }
306
+
307
+ /**
308
+ * Export report as CSV
309
+ * @private
310
+ */
311
+ function exportA11yReportAsCsv(report) {
312
+ const headers = ['severity', 'rule', 'message', 'element', 'selector'];
313
+ const rows = report.issues.map(i =>
314
+ [i.severity, i.rule, `"${i.message.replace(/"/g, '""')}"`, i.element, `"${i.selector}"`].join(',')
315
+ );
316
+
317
+ return [headers.join(','), ...rows].join('\n');
318
+ }
319
+
320
+ /**
321
+ * Export report as HTML
322
+ * @private
323
+ */
324
+ function exportA11yReportAsHtml(report) {
325
+ const errorCount = report.stats.errorCount;
326
+ const warningCount = report.stats.warningCount;
327
+
328
+ return `<!DOCTYPE html>
329
+ <html lang="en">
330
+ <head>
331
+ <meta charset="UTF-8">
332
+ <title>Accessibility Audit Report</title>
333
+ <style>
334
+ body { font-family: system-ui, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; }
335
+ h1 { color: #333; }
336
+ .summary { display: flex; gap: 20px; margin: 20px 0; }
337
+ .stat { padding: 15px 25px; border-radius: 8px; }
338
+ .errors { background: #fee; color: #c00; }
339
+ .warnings { background: #ffd; color: #a50; }
340
+ table { width: 100%; border-collapse: collapse; margin-top: 20px; }
341
+ th, td { text-align: left; padding: 10px; border-bottom: 1px solid #ddd; }
342
+ th { background: #f5f5f5; }
343
+ .error { color: #c00; }
344
+ .warning { color: #a50; }
345
+ code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
346
+ </style>
347
+ </head>
348
+ <body>
349
+ <h1>Accessibility Audit Report</h1>
350
+ <p>Generated: ${report.timestamp}</p>
351
+ <p>URL: <code>${report.url}</code></p>
352
+
353
+ <div class="summary">
354
+ <div class="stat errors"><strong>${errorCount}</strong> Errors</div>
355
+ <div class="stat warnings"><strong>${warningCount}</strong> Warnings</div>
356
+ </div>
357
+
358
+ ${report.issues.length > 0 ? `
359
+ <table>
360
+ <thead>
361
+ <tr>
362
+ <th>Severity</th>
363
+ <th>Rule</th>
364
+ <th>Message</th>
365
+ <th>Element</th>
366
+ </tr>
367
+ </thead>
368
+ <tbody>
369
+ ${report.issues.map(i => `
370
+ <tr>
371
+ <td class="${i.severity}">${i.severity}</td>
372
+ <td><code>${i.rule}</code></td>
373
+ <td>${i.message}</td>
374
+ <td><code>${i.selector}</code></td>
375
+ </tr>
376
+ `).join('')}
377
+ </tbody>
378
+ </table>
379
+ ` : '<p style="color: green;">✓ No accessibility issues found!</p>'}
380
+ </body>
381
+ </html>`;
382
+ }
383
+
384
+ /**
385
+ * Export a11y audit report
386
+ * @param {string} [format='json'] - Export format ('json', 'csv', 'html')
387
+ * @returns {string} Formatted report
388
+ */
389
+ export function exportA11yReport(format = 'json') {
390
+ const stats = getA11yStats();
391
+ const issues = getA11yIssues();
392
+
393
+ const report = {
394
+ timestamp: new Date().toISOString(),
395
+ url: typeof location !== 'undefined' ? location.href : 'unknown',
396
+ stats,
397
+ issues: issues.map(i => ({
398
+ rule: i.rule,
399
+ severity: i.severity,
400
+ message: i.message,
401
+ element: i.element?.tagName?.toLowerCase() || 'unknown',
402
+ selector: i.element ? getElementSelector(i.element) : 'unknown'
403
+ }))
404
+ };
405
+
406
+ switch (format) {
407
+ case 'csv':
408
+ return exportA11yReportAsCsv(report);
409
+ case 'html':
410
+ return exportA11yReportAsHtml(report);
411
+ default:
412
+ return JSON.stringify(report, null, 2);
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Reset a11y audit state
418
+ */
419
+ export function resetA11yAudit() {
420
+ disableA11yAudit();
421
+ a11yAuditState = {
422
+ issues: [],
423
+ lastAuditTime: null,
424
+ auditCount: 0,
425
+ highlightCleanup: null,
426
+ mutationObserver: null,
427
+ intervalId: null,
428
+ mutationTimeout: null
429
+ };
430
+ }
431
+
432
+ export default {
433
+ a11yAuditConfig,
434
+ runA11yAudit,
435
+ getA11yIssues,
436
+ getA11yStats,
437
+ enableA11yAudit,
438
+ disableA11yAudit,
439
+ toggleA11yHighlights,
440
+ exportA11yReport,
441
+ resetA11yAudit
442
+ };