snap-ally 0.2.7-beta → 0.3.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.
Files changed (75) hide show
  1. package/dist/A11yReportAssets.d.ts +5 -12
  2. package/dist/A11yReportAssets.js +16 -82
  3. package/dist/A11yScanner.d.ts +2 -21
  4. package/dist/A11yScanner.js +16 -22
  5. package/dist/A11yTimeUtils.js +11 -23
  6. package/dist/A11yVisualReporter.d.ts +50 -0
  7. package/dist/A11yVisualReporter.js +188 -0
  8. package/dist/AccessibilityReporterOptions.d.ts +24 -0
  9. package/dist/AccessibilityReporterOptions.js +5 -0
  10. package/dist/ResolvedColors.d.ts +15 -0
  11. package/dist/ResolvedColors.js +20 -0
  12. package/dist/SnapAllyReporter.d.ts +12 -72
  13. package/dist/SnapAllyReporter.js +218 -329
  14. package/dist/core/A11yHtmlRenderer.d.ts +17 -0
  15. package/dist/core/A11yHtmlRenderer.js +118 -0
  16. package/dist/core/A11yReportAssets.d.ts +30 -0
  17. package/dist/core/A11yReportAssets.js +127 -0
  18. package/dist/core/A11yScanner.d.ts +8 -0
  19. package/dist/core/A11yScanner.js +178 -0
  20. package/dist/core/A11yVisualReporter.d.ts +50 -0
  21. package/dist/core/A11yVisualReporter.js +188 -0
  22. package/dist/core/HtmlRenderer.d.ts +14 -0
  23. package/dist/core/HtmlRenderer.js +106 -0
  24. package/dist/core/ReportAssets.d.ts +29 -0
  25. package/dist/core/ReportAssets.js +126 -0
  26. package/dist/core/Scanner.d.ts +7 -0
  27. package/dist/core/Scanner.js +162 -0
  28. package/dist/core/VisualReporter.d.ts +54 -0
  29. package/dist/core/VisualReporter.js +192 -0
  30. package/dist/index.d.ts +6 -6
  31. package/dist/index.js +13 -12
  32. package/dist/models/A11yDataSource.d.ts +15 -0
  33. package/dist/models/A11yDataSource.js +2 -0
  34. package/dist/models/A11yError.d.ts +34 -0
  35. package/dist/models/A11yError.js +11 -0
  36. package/dist/models/A11yScannerOptions.d.ts +24 -0
  37. package/dist/models/A11yScannerOptions.js +2 -0
  38. package/dist/models/AccessibilityReporterOptions.d.ts +24 -0
  39. package/dist/models/AccessibilityReporterOptions.js +5 -0
  40. package/dist/models/DataSource.d.ts +15 -0
  41. package/dist/models/DataSource.js +2 -0
  42. package/dist/models/ImagePath.d.ts +5 -0
  43. package/dist/models/ImagePath.js +3 -0
  44. package/dist/models/ReportData.d.ts +24 -0
  45. package/dist/models/ReportData.js +2 -0
  46. package/dist/models/ReporterOptions.d.ts +24 -0
  47. package/dist/models/ReporterOptions.js +5 -0
  48. package/dist/models/ResolvedColors.d.ts +16 -0
  49. package/dist/models/ResolvedColors.js +24 -0
  50. package/dist/models/ScannerOptions.d.ts +30 -0
  51. package/dist/models/ScannerOptions.js +2 -0
  52. package/dist/models/Severity.d.ts +7 -0
  53. package/dist/models/Severity.js +11 -0
  54. package/dist/models/Target.d.ts +10 -0
  55. package/dist/models/Target.js +3 -0
  56. package/dist/models/TestResults.d.ts +41 -0
  57. package/dist/models/TestResults.js +2 -0
  58. package/dist/models/TestStatusIcon.d.ts +8 -0
  59. package/dist/models/TestStatusIcon.js +12 -0
  60. package/dist/models/TestSummary.d.ts +34 -0
  61. package/dist/models/TestSummary.js +2 -0
  62. package/dist/models/Violation.d.ts +13 -0
  63. package/dist/models/Violation.js +2 -0
  64. package/dist/models/index.d.ts +12 -113
  65. package/dist/models/index.js +26 -16
  66. package/dist/templates/accessibility-report.html +62 -95
  67. package/dist/templates/execution-summary.html +37 -103
  68. package/dist/templates/global-report-styles.css +400 -9
  69. package/dist/templates/report-app.js +170 -72
  70. package/dist/templates/test-execution-report.html +84 -121
  71. package/dist/utils/A11yTimeUtils.d.ts +13 -0
  72. package/dist/utils/A11yTimeUtils.js +40 -0
  73. package/dist/utils/TimeUtils.d.ts +13 -0
  74. package/dist/utils/TimeUtils.js +39 -0
  75. package/package.json +2 -2
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.A11yVisualReporter = void 0;
4
+ /**
5
+ * Manages visual feedback on the page during an accessibility scan.
6
+ * Handles element highlights, violation banners, and report attachments.
7
+ */
8
+ class A11yVisualReporter {
9
+ constructor(page) {
10
+ this.page = page;
11
+ this.rootId = 'snap-ally-visual-root';
12
+ }
13
+ /**
14
+ * Shows a violation banner at the bottom of the page.
15
+ */
16
+ async showBanner(violation, color) {
17
+ await this.safeEvaluate(([v, rawColor, rootId, bannerId]) => {
18
+ const shadow = window.snapAllyGetRoot(rootId);
19
+ let container = shadow.getElementById(bannerId);
20
+ if (!container) {
21
+ const style = document.createElement('style');
22
+ style.textContent = `
23
+ #${bannerId} {
24
+ position: fixed;
25
+ left: 50%;
26
+ top: 24px;
27
+ transform: translateX(-50%);
28
+ width: calc(100% - 40px);
29
+ max-width: 600px;
30
+ padding: 12px 18px;
31
+ border-radius: 12px;
32
+ color: white;
33
+ font-family: system-ui, -apple-system, sans-serif;
34
+ font-size: 14px;
35
+ display: flex;
36
+ align-items: center;
37
+ gap: 12px;
38
+ box-shadow: 0 12px 40px rgba(0,0,0,0.3);
39
+ backdrop-filter: blur(16px) saturate(180%);
40
+ -webkit-backdrop-filter: blur(16px) saturate(180%);
41
+ border: 1px solid rgba(255,255,255,0.15);
42
+ z-index: 10000;
43
+ transition: all 0.3s ease;
44
+ }
45
+ .badge {
46
+ background: rgba(255,255,255,0.2);
47
+ padding: 2px 8px;
48
+ border-radius: 6px;
49
+ font-size: 11px;
50
+ font-weight: 700;
51
+ text-transform: uppercase;
52
+ border: 1px solid rgba(255,255,255,0.2);
53
+ }
54
+ .content { flex: 1; line-height: 1.4; font-weight: 500; }
55
+ `;
56
+ shadow.appendChild(style);
57
+ container = document.createElement('div');
58
+ container.id = bannerId;
59
+ shadow.appendChild(container);
60
+ }
61
+ container.style.backgroundColor = window.snapAllyToAlpha(rawColor, 0.85);
62
+ container.innerHTML = `
63
+ <div style="font-size: 20px;">⚠️</div>
64
+ <div class="content">
65
+ <div style="margin-bottom:4px; display:flex; align-items:center; gap:8px;">
66
+ <span class="badge">${v.id}</span>
67
+ <span style="opacity: 0.9;">${v.help}</span>
68
+ </div>
69
+ </div>
70
+ `;
71
+ }, [violation, color, this.rootId, A11yVisualReporter.BANNER_ID]);
72
+ }
73
+ /**
74
+ * Highlights an element on the page.
75
+ */
76
+ async highlightElement(selector, color) {
77
+ await this.safeEvaluate(([sel, rawColor, rootId, highlightId]) => {
78
+ const target = document.querySelector(sel);
79
+ if (!target)
80
+ return;
81
+ target.scrollIntoView({ behavior: 'auto', block: 'center' });
82
+ const shadow = window.snapAllyGetRoot(rootId);
83
+ let highlight = shadow.getElementById(highlightId);
84
+ if (!highlight) {
85
+ const style = document.createElement('style');
86
+ style.textContent = `
87
+ #${highlightId} {
88
+ position: absolute;
89
+ pointer-events: none;
90
+ border-radius: 8px;
91
+ box-sizing: border-box;
92
+ z-index: 9999;
93
+ transition: all 0.2s ease;
94
+ box-shadow: 0 0 0 4px var(--c-alpha), 0 0 20px var(--c-alpha);
95
+ }
96
+ `;
97
+ shadow.appendChild(style);
98
+ highlight = document.createElement('div');
99
+ highlight.id = highlightId;
100
+ shadow.appendChild(highlight);
101
+ }
102
+ const pad = 4;
103
+ const rect = target.getBoundingClientRect();
104
+ highlight.style.left = `${rect.left + window.scrollX - pad}px`;
105
+ highlight.style.top = `${rect.top + window.scrollY - pad}px`;
106
+ highlight.style.width = `${rect.width + pad * 2}px`;
107
+ highlight.style.height = `${rect.height + pad * 2}px`;
108
+ highlight.style.border = `3px solid ${rawColor}`;
109
+ highlight.style.setProperty('--c-alpha', window.snapAllyToAlpha(rawColor, 0.3));
110
+ }, [selector, color, this.rootId, A11yVisualReporter.HIGHLIGHT_ID]);
111
+ }
112
+ /**
113
+ * Removes all visual feedback from the page.
114
+ */
115
+ async clean() {
116
+ await this.safeEvaluate((id) => { var _a; return (_a = document.getElementById(id)) === null || _a === void 0 ? void 0 : _a.remove(); }, this.rootId);
117
+ }
118
+ /**
119
+ * Removes the element highlight.
120
+ */
121
+ async removeHighlight() {
122
+ await this.safeEvaluate(([rootId, hId]) => { var _a, _b, _c; return (_c = (_b = (_a = document.getElementById(rootId)) === null || _a === void 0 ? void 0 : _a.shadowRoot) === null || _b === void 0 ? void 0 : _b.getElementById(hId)) === null || _c === void 0 ? void 0 : _c.remove(); }, [this.rootId, A11yVisualReporter.HIGHLIGHT_ID]);
123
+ }
124
+ /**
125
+ * Attaches JSON data to the test report.
126
+ */
127
+ async attachData(testInfo, name, data) {
128
+ await testInfo.attach(name, {
129
+ contentType: 'application/json',
130
+ body: Buffer.from(data),
131
+ });
132
+ }
133
+ /**
134
+ * Captures and attaches a screenshot to the test report.
135
+ */
136
+ async captureScreenshot(name, testInfo) {
137
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
138
+ const { test } = require('@playwright/test');
139
+ return await test.step('Capture A11y screenshot', async () => {
140
+ const screenshot = await this.page.screenshot({ fullPage: false });
141
+ await testInfo.attach(name, { contentType: 'image/png', body: screenshot });
142
+ return screenshot;
143
+ });
144
+ }
145
+ /**
146
+ * Injects helper functions into the page context.
147
+ */
148
+ async ensureHelpers() {
149
+ await this.page.evaluate(() => {
150
+ if (typeof window.snapAllyGetRoot === 'function')
151
+ return;
152
+ window.snapAllyGetRoot = (id) => {
153
+ let root = document.getElementById(id);
154
+ if (!root) {
155
+ root = document.createElement('div');
156
+ root.id = id;
157
+ root.style.cssText = 'position:absolute;top:0;left:0;width:0;height:0;z-index:2147483647;';
158
+ document.body.appendChild(root);
159
+ root.attachShadow({ mode: 'open' });
160
+ }
161
+ return root.shadowRoot;
162
+ };
163
+ window.snapAllyToAlpha = (color, alpha) => {
164
+ if (color.startsWith('rgba'))
165
+ return color.replace(/[\d.]+\)$/, `${alpha})`);
166
+ if (color.startsWith('rgb'))
167
+ return color.replace('rgb', 'rgba').replace(')', `, ${alpha})`);
168
+ const hex = Math.round(alpha * 255).toString(16).padStart(2, '0');
169
+ return `${color}${hex}`;
170
+ };
171
+ });
172
+ }
173
+ async safeEvaluate(fn, arg) {
174
+ try {
175
+ await this.ensureHelpers();
176
+ await (arg !== undefined ? this.page.evaluate(fn, arg) : this.page.evaluate(fn));
177
+ }
178
+ catch (e) {
179
+ const msg = e instanceof Error ? e.message : String(e);
180
+ if (msg.includes('closed') || msg.includes('destroyed') || msg.includes('ended'))
181
+ return;
182
+ throw e;
183
+ }
184
+ }
185
+ }
186
+ exports.A11yVisualReporter = A11yVisualReporter;
187
+ A11yVisualReporter.BANNER_ID = 'snap-ally-banner';
188
+ A11yVisualReporter.HIGHLIGHT_ID = 'snap-ally-highlight';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Handles the rendering of HTML reports using static templates and JSON data injection.
3
+ */
4
+ export declare class HtmlRenderer {
5
+ /**
6
+ * Renders a static HTML template by copying it and generating the accompanied data payload.
7
+ */
8
+ render(templateName: string, data: Record<string, unknown>, _outputFolder: string, // Kept for signature compatibility
9
+ outputFileName: string): Promise<void>;
10
+ /**
11
+ * Converts ANSI color codes to HTML spans for nicer error display.
12
+ */
13
+ ansiToHtml(text: string): string;
14
+ }
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.HtmlRenderer = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ /**
40
+ * Handles the rendering of HTML reports using static templates and JSON data injection.
41
+ */
42
+ class HtmlRenderer {
43
+ /**
44
+ * Renders a static HTML template by copying it and generating the accompanied data payload.
45
+ */
46
+ async render(templateName, data, _outputFolder, // Kept for signature compatibility
47
+ outputFileName) {
48
+ // Resolve path relative to this file (dist/core/HtmlRenderer.js)
49
+ const templatesDir = path.join(__dirname, '..', 'templates');
50
+ const templatePath = path.join(templatesDir, templateName);
51
+ const cssPath = path.join(templatesDir, 'global-report-styles.css');
52
+ const jsPath = path.join(templatesDir, 'report-app.js');
53
+ if (!fs.existsSync(templatePath)) {
54
+ throw new Error(`[HtmlRenderer] Template not found: ${templatePath}`);
55
+ }
56
+ let html = fs.readFileSync(templatePath, 'utf8');
57
+ // Inline CSS
58
+ if (fs.existsSync(cssPath)) {
59
+ const css = fs.readFileSync(cssPath, 'utf8');
60
+ html = html.replace('</head>', `<style>\n${css}\n</style>\n</head>`);
61
+ // Remove the link tag if it exists
62
+ html = html.replace(/<link[^>]*global-report-styles\.css[^>]*>/, '');
63
+ }
64
+ // Inline Data
65
+ const jsData = `window.snapAllyData = ${JSON.stringify(data)};`;
66
+ html = html.replace('<script src="data.js"></script>', `<script>\n${jsData}\n</script>`);
67
+ // Inline main logic
68
+ if (fs.existsSync(jsPath)) {
69
+ const js = fs.readFileSync(jsPath, 'utf8');
70
+ html = html.replace('<script src="report-app.js"></script>', `<script>\n${js}\n</script>`);
71
+ }
72
+ // Final cleanup of any potential remaining dummy scripts
73
+ html = html.replace(/<script src="data-[^>]*\.js"><\/script>/, '');
74
+ // Ensure the output directory exists before writing
75
+ const outputDir = path.dirname(outputFileName);
76
+ if (!fs.existsSync(outputDir)) {
77
+ fs.mkdirSync(outputDir, { recursive: true });
78
+ }
79
+ fs.writeFileSync(outputFileName, html, 'utf8');
80
+ }
81
+ /**
82
+ * Converts ANSI color codes to HTML spans for nicer error display.
83
+ */
84
+ ansiToHtml(text) {
85
+ const map = {
86
+ '\u001b[30m': '<span style="color:black">',
87
+ '\u001b[31m': '<span style="color:red">',
88
+ '\u001b[32m': '<span style="color:green">',
89
+ '\u001b[33m': '<span style="color:yellow">',
90
+ '\u001b[34m': '<span style="color:blue">',
91
+ '\u001b[35m': '<span style="color:magenta">',
92
+ '\u001b[36m': '<span style="color:cyan">',
93
+ '\u001b[37m': '<span style="color:white">',
94
+ '\u001b[0m': '</span>',
95
+ '\u001b[2m': '<span style="opacity:0.5">',
96
+ '\u001b[22m': '</span>',
97
+ '\u001b[39m': '</span>',
98
+ };
99
+ let result = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
100
+ for (const [code, tag] of Object.entries(map)) {
101
+ result = result.split(code).join(tag);
102
+ }
103
+ return result;
104
+ }
105
+ }
106
+ exports.HtmlRenderer = HtmlRenderer;
@@ -0,0 +1,29 @@
1
+ import { TestResult } from '@playwright/test/reporter';
2
+ /**
3
+ * Utilities for managing and copying report assets like videos and screenshots.
4
+ */
5
+ export declare class ReportAssets {
6
+ /**
7
+ * Copies a file from source to a destination folder.
8
+ */
9
+ copyToFolder(destFolder: string, srcPath: string, fileName?: string): string;
10
+ /**
11
+ * Copies all video attachments to the report folder for portability.
12
+ */
13
+ copyVideos(result: TestResult, destFolder: string): string[];
14
+ /**
15
+ * Copies all screenshots found in the test attachments.
16
+ */
17
+ copyScreenshots(result: TestResult, destFolder: string): string[];
18
+ /**
19
+ * Copies all other attachments (traces, logs, etc.) to the report folder.
20
+ */
21
+ copyAllOtherAttachments(result: TestResult, destFolder: string): {
22
+ path: string;
23
+ name: string;
24
+ }[];
25
+ /**
26
+ * Persists an in-memory buffer to a file in the destination folder.
27
+ */
28
+ saveBuffer(destFolder: string, fileName: string, buffer: Buffer): string;
29
+ }
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ReportAssets = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ /**
40
+ * Utilities for managing and copying report assets like videos and screenshots.
41
+ */
42
+ class ReportAssets {
43
+ /**
44
+ * Copies a file from source to a destination folder.
45
+ */
46
+ copyToFolder(destFolder, srcPath, fileName) {
47
+ if (!srcPath || !fs.existsSync(srcPath)) {
48
+ return '';
49
+ }
50
+ const name = fileName || path.basename(srcPath);
51
+ const destFile = path.join(destFolder, name);
52
+ if (!fs.existsSync(destFolder)) {
53
+ fs.mkdirSync(destFolder, { recursive: true });
54
+ }
55
+ fs.copyFileSync(srcPath, destFile);
56
+ return name;
57
+ }
58
+ /**
59
+ * Copies all video attachments to the report folder for portability.
60
+ */
61
+ copyVideos(result, destFolder) {
62
+ return result.attachments
63
+ .filter((a) => (a.name === 'video' || (a.contentType || '').startsWith('video/')) && a.path)
64
+ .map((attachment) => this.copyToFolder(destFolder, attachment.path))
65
+ .filter((p) => !!p);
66
+ }
67
+ /**
68
+ * Copies all screenshots found in the test attachments.
69
+ */
70
+ copyScreenshots(result, destFolder) {
71
+ return result.attachments
72
+ .filter((a) => a.name === 'screenshot' ||
73
+ a.name.endsWith('.png') ||
74
+ (a.contentType || '').startsWith('image/'))
75
+ .map((a) => {
76
+ if (a.path) {
77
+ return this.copyToFolder(destFolder, a.path, a.name !== 'screenshot' ? a.name : undefined);
78
+ }
79
+ else if (a.body) {
80
+ const timestamp = Date.now();
81
+ const name = a.name === 'screenshot'
82
+ ? `screenshot-${timestamp}.png`
83
+ : a.name.endsWith('.png')
84
+ ? a.name
85
+ : `${a.name}.png`;
86
+ return this.saveBuffer(destFolder, name, a.body);
87
+ }
88
+ return '';
89
+ })
90
+ .filter((path) => path !== '');
91
+ }
92
+ /**
93
+ * Copies all other attachments (traces, logs, etc.) to the report folder.
94
+ */
95
+ copyAllOtherAttachments(result, destFolder) {
96
+ const excludedNames = ['screenshot', 'video', 'A11y'];
97
+ return result.attachments
98
+ .filter((a) => !excludedNames.includes(a.name) &&
99
+ !a.name.toLowerCase().endsWith('.png') &&
100
+ !(a.contentType || '').startsWith('image/') &&
101
+ !(a.contentType || '').startsWith('video/'))
102
+ .map((a) => {
103
+ let name = '';
104
+ if (a.path) {
105
+ name = this.copyToFolder(destFolder, a.path, a.name);
106
+ }
107
+ else if (a.body) {
108
+ name = this.saveBuffer(destFolder, a.name, a.body);
109
+ }
110
+ return name ? { path: name, name: a.name } : null;
111
+ })
112
+ .filter((item) => item !== null);
113
+ }
114
+ /**
115
+ * Persists an in-memory buffer to a file in the destination folder.
116
+ */
117
+ saveBuffer(destFolder, fileName, buffer) {
118
+ if (!fs.existsSync(destFolder)) {
119
+ fs.mkdirSync(destFolder, { recursive: true });
120
+ }
121
+ const destFile = path.join(destFolder, fileName);
122
+ fs.writeFileSync(destFile, buffer);
123
+ return fileName;
124
+ }
125
+ }
126
+ exports.ReportAssets = ReportAssets;
@@ -0,0 +1,7 @@
1
+ import type { Page, TestInfo } from '@playwright/test';
2
+ import { ScannerOptions } from '../models';
3
+ /**
4
+ * Performs an accessibility audit using Axe and Lighthouse.
5
+ */
6
+ export declare function scanA11y(page: Page, testInfo: TestInfo, options?: ScannerOptions): Promise<void>;
7
+ export declare const checkAccessibility: typeof scanA11y;
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkAccessibility = void 0;
7
+ exports.scanA11y = scanA11y;
8
+ const playwright_1 = __importDefault(require("@axe-core/playwright"));
9
+ const test_1 = require("@playwright/test");
10
+ const VisualReporter_1 = require("./VisualReporter");
11
+ const models_1 = require("../models");
12
+ const TimeUtils_1 = require("../utils/TimeUtils");
13
+ /**
14
+ * Sanitizes a string to be safe for use in file paths and prevents path traversal attacks.
15
+ */
16
+ function sanitizePageKey(input) {
17
+ return (input
18
+ .replace(/^https?:\/\//, '')
19
+ .replace(/[\/\\:*?"<>|]/g, '-')
20
+ .replace(/\.\./g, '')
21
+ .replace(/-+/g, '-')
22
+ .replace(/^-+|-+$/g, '')
23
+ .toLowerCase()
24
+ .substring(0, 200));
25
+ }
26
+ /**
27
+ * Performs an accessibility audit using Axe and Lighthouse.
28
+ */
29
+ async function scanA11y(page, testInfo, options = {}) {
30
+ var _a, _b, _c, _d, _e, _f;
31
+ const showTerminal = (_a = options.verbose) !== null && _a !== void 0 ? _a : true;
32
+ const showBrowser = (_b = options.consoleLog) !== null && _b !== void 0 ? _b : true;
33
+ const rawPageKey = options.pageKey || page.url();
34
+ const pageKey = sanitizePageKey(rawPageKey);
35
+ const overlay = new VisualReporter_1.VisualReporter(page);
36
+ let axeBuilder = new playwright_1.default({ page });
37
+ const target = options.include || options.box;
38
+ if (target) {
39
+ if (typeof target === 'string') {
40
+ axeBuilder = axeBuilder.include(target);
41
+ }
42
+ else {
43
+ axeBuilder = axeBuilder.include(target);
44
+ }
45
+ }
46
+ if (options.rules) {
47
+ axeBuilder = axeBuilder.options({ rules: options.rules });
48
+ }
49
+ if (options.tags) {
50
+ axeBuilder = axeBuilder.withTags(options.tags);
51
+ }
52
+ if (options.axeOptions) {
53
+ axeBuilder = axeBuilder.options(options.axeOptions);
54
+ }
55
+ let axeResults;
56
+ try {
57
+ axeResults = await axeBuilder.analyze();
58
+ }
59
+ catch (error) {
60
+ if (error instanceof Error &&
61
+ (error.message.includes('Test ended') ||
62
+ error.message.includes('Target page, context or browser has been closed'))) {
63
+ console.warn(`[SnapAlly] Accessibility scan skipped: ${error.message}`);
64
+ return;
65
+ }
66
+ throw error;
67
+ }
68
+ const violationCount = axeResults.violations.length;
69
+ if ((showTerminal || showBrowser) && violationCount > 0) {
70
+ const mainMsg = `[A11yScanner] Violations found: ${violationCount}`;
71
+ const detailMessages = axeResults.violations.map(
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ (v, i) => ` ${i + 1}. ${v.id} [${v.impact}] - ${v.help}`);
74
+ if (showTerminal) {
75
+ console.log(`\n${mainMsg}`);
76
+ detailMessages.forEach((msg) => console.log(msg));
77
+ }
78
+ if (showBrowser) {
79
+ await page.evaluate(([mainMsg, details, color]) => {
80
+ console.log(`%c ${mainMsg}`, `color: ${color}; font-weight: bold; font-size: 12px;`);
81
+ details.forEach((msg) => console.log(msg));
82
+ }, [mainMsg, detailMessages, models_1.DEFAULT_COLORS.serious]);
83
+ }
84
+ }
85
+ test_1.expect
86
+ .soft(violationCount, `Accessibility audit failed with ${violationCount} violations.`)
87
+ .toBe(0);
88
+ const reporterConfig = testInfo.config.reporter.find((r) => Array.isArray(r) &&
89
+ (typeof r[0] === 'string' &&
90
+ (r[0].includes('SnapAllyReporter') || r[0].endsWith('src/SnapAllyReporter.ts'))));
91
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
+ const customColors = reporterConfig && Array.isArray(reporterConfig) ? (_c = reporterConfig[1]) === null || _c === void 0 ? void 0 : _c.colors : undefined;
93
+ const errors = [];
94
+ for (const violation of axeResults.violations) {
95
+ let errorIdx = 0;
96
+ const targets = [];
97
+ const severityColor = (0, models_1.getSeverityColor)(violation.impact, customColors);
98
+ for (const node of violation.nodes) {
99
+ for (const selector of node.target) {
100
+ const elementSelector = selector.toString();
101
+ const locator = page.locator(elementSelector);
102
+ await overlay.showBanner({ id: violation.id, help: violation.help }, severityColor);
103
+ if (await locator.isVisible()) {
104
+ await overlay.highlightElement(elementSelector, severityColor);
105
+ // eslint-disable-next-line playwright/no-wait-for-timeout
106
+ await page.waitForTimeout(100);
107
+ const screenshotName = `a11y-${violation.id}-${errorIdx++}.png`;
108
+ const buffer = await overlay.captureScreenshot(testInfo, screenshotName);
109
+ const excluded = new Set([
110
+ 'Pre Condition',
111
+ 'Post Condition',
112
+ 'Description',
113
+ 'A11y',
114
+ ]);
115
+ const contextSteps = (testInfo.annotations || [])
116
+ .filter((a) => !excluded.has(a.type))
117
+ .map((a) => a.description || '');
118
+ const nodeHtml = node.html || '';
119
+ const friendlySnippet = elementSelector;
120
+ targets.push({
121
+ element: elementSelector,
122
+ snippet: friendlySnippet,
123
+ html: nodeHtml,
124
+ screenshot: screenshotName,
125
+ steps: contextSteps,
126
+ stepsJson: JSON.stringify(contextSteps),
127
+ screenshotBase64: buffer.toString('base64'),
128
+ });
129
+ await overlay.removeHighlight();
130
+ }
131
+ }
132
+ }
133
+ errors.push({
134
+ id: violation.id,
135
+ description: violation.description,
136
+ severity: violation.impact || 'unknown',
137
+ helpUrl: violation.helpUrl,
138
+ help: violation.help,
139
+ guideline: violation.tags[1] || 'N/A',
140
+ wcagRule: violation.tags.find((t) => t.startsWith('wcag')) || violation.tags[1] || 'N/A',
141
+ total: targets.length || violation.nodes.length,
142
+ target: targets,
143
+ });
144
+ }
145
+ const reportData = {
146
+ pageKey,
147
+ pageUrl: page.url(),
148
+ accessibilityScore: 0,
149
+ a11yErrors: errors,
150
+ criticalColor: (customColors === null || customColors === void 0 ? void 0 : customColors.critical) || models_1.DEFAULT_COLORS.critical,
151
+ seriousColor: (customColors === null || customColors === void 0 ? void 0 : customColors.serious) || models_1.DEFAULT_COLORS.serious,
152
+ moderateColor: (customColors === null || customColors === void 0 ? void 0 : customColors.moderate) || models_1.DEFAULT_COLORS.moderate,
153
+ minorColor: (customColors === null || customColors === void 0 ? void 0 : customColors.minor) || models_1.DEFAULT_COLORS.minor,
154
+ adoOrganization: ((_d = options.ado) === null || _d === void 0 ? void 0 : _d.organization) || process.env.ADO_ORGANIZATION || '',
155
+ adoProject: ((_e = options.ado) === null || _e === void 0 ? void 0 : _e.project) || process.env.ADO_PROJECT || '',
156
+ adoAreaPath: ((_f = options.ado) === null || _f === void 0 ? void 0 : _f.areaPath) || process.env.ADO_AREA_PATH || '',
157
+ timestamp: TimeUtils_1.TimeUtils.formatDate(new Date()),
158
+ };
159
+ await overlay.attachJsonData(testInfo, 'A11y', JSON.stringify(reportData));
160
+ await overlay.cleanupOverlay();
161
+ }
162
+ exports.checkAccessibility = scanA11y;