snap-ally 0.2.5-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 +75 -101
  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 +171 -73
  70. package/dist/templates/test-execution-report.html +84 -113
  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
@@ -8,21 +8,14 @@ export declare class A11yReportAssets {
8
8
  */
9
9
  copyToFolder(destFolder: string, srcPath: string, fileName?: string): string;
10
10
  /**
11
- * Copies the test video if available.
12
- * Includes a more robust retry to ensure Playwright has finished flushing the file.
11
+ * Copies all video attachments to the report folder for portability.
12
+ * @returns An array of filenames written to the destination folder.
13
13
  */
14
- copyTestVideo(result: TestResult, destFolder: string): Promise<string>;
14
+ copyVideos(result: TestResult, destFolder: string): string[];
15
15
  /**
16
16
  * Copies all screenshots found in the test attachments.
17
17
  */
18
18
  copyScreenshots(result: TestResult, destFolder: string): string[];
19
- /**
20
- * Copies all PNG attachments to the report folder and returns their new names.
21
- */
22
- copyPngAttachments(result: TestResult, destFolder: string): {
23
- path: string;
24
- name: string;
25
- }[];
26
19
  /**
27
20
  * Copies all other attachments (traces, logs, etc.) to the report folder.
28
21
  */
@@ -31,7 +24,7 @@ export declare class A11yReportAssets {
31
24
  name: string;
32
25
  }[];
33
26
  /**
34
- * Writes a buffer to a file in the destination folder.
27
+ * Persists an in-memory buffer to a file in the destination folder.
35
28
  */
36
- writeBuffer(destFolder: string, fileName: string, buffer: Buffer): string;
29
+ saveBuffer(destFolder: string, fileName: string, buffer: Buffer): string;
37
30
  }
@@ -56,72 +56,26 @@ class A11yReportAssets {
56
56
  return name;
57
57
  }
58
58
  /**
59
- * Copies the test video if available.
60
- * Includes a more robust retry to ensure Playwright has finished flushing the file.
59
+ * Copies all video attachments to the report folder for portability.
60
+ * @returns An array of filenames written to the destination folder.
61
61
  */
62
- async copyTestVideo(result, destFolder) {
63
- // More flexible matching for video attachments
64
- const videoAttachments = result.attachments.filter((a) => a.name === 'video' || (a.contentType || '').startsWith('video/'));
65
- let bestVideo = null;
66
- let maxSize = -1;
67
- for (const attachment of videoAttachments) {
68
- if (!attachment.path)
69
- continue;
70
- // Retry logic: Wait for file to exist and have non-zero size (up to 3 seconds)
71
- let attempts = 0;
72
- let isReady = false;
73
- while (attempts < 15) {
74
- if (fs.existsSync(attachment.path)) {
75
- try {
76
- const stats = fs.statSync(attachment.path);
77
- if (stats.size > 0) {
78
- isReady = true;
79
- break;
80
- }
81
- }
82
- catch {
83
- // statSync might fail if file is temporarily locked
84
- }
85
- }
86
- await new Promise((r) => setTimeout(r, 200));
87
- attempts++;
88
- }
89
- if (isReady) {
90
- try {
91
- const size = fs.statSync(attachment.path).size;
92
- if (size > maxSize) {
93
- maxSize = size;
94
- bestVideo = attachment.path;
95
- }
96
- }
97
- catch (err) {
98
- console.error(`[SnapAlly] Error checking video stats: ${err}`);
99
- }
100
- }
101
- else {
102
- console.warn(`[SnapAlly] Video attachment found but file is missing or empty after retry: ${attachment.path}`);
103
- }
104
- }
105
- if (bestVideo) {
106
- try {
107
- return this.copyToFolder(destFolder, bestVideo, 'video.webm');
108
- }
109
- catch (e) {
110
- console.error(`[SnapAlly] Failed to copy video: ${e}`);
111
- return path.basename(bestVideo);
112
- }
113
- }
114
- return '';
62
+ copyVideos(result, destFolder) {
63
+ return result.attachments
64
+ .filter((a) => (a.name === 'video' || (a.contentType || '').startsWith('video/')) && a.path)
65
+ .map((attachment) => this.copyToFolder(destFolder, attachment.path))
66
+ .filter((p) => !!p);
115
67
  }
116
68
  /**
117
69
  * Copies all screenshots found in the test attachments.
118
70
  */
119
71
  copyScreenshots(result, destFolder) {
120
72
  return result.attachments
121
- .filter((a) => a.name === 'screenshot' || (a.contentType || '').startsWith('image/'))
73
+ .filter((a) => a.name === 'screenshot' ||
74
+ a.name.endsWith('.png') ||
75
+ (a.contentType || '').startsWith('image/'))
122
76
  .map((a) => {
123
77
  if (a.path) {
124
- return this.copyToFolder(destFolder, a.path);
78
+ return this.copyToFolder(destFolder, a.path, a.name !== 'screenshot' ? a.name : undefined);
125
79
  }
126
80
  else if (a.body) {
127
81
  const timestamp = Date.now();
@@ -130,32 +84,12 @@ class A11yReportAssets {
130
84
  : a.name.endsWith('.png')
131
85
  ? a.name
132
86
  : `${a.name}.png`;
133
- return this.writeBuffer(destFolder, name, a.body);
87
+ return this.saveBuffer(destFolder, name, a.body);
134
88
  }
135
89
  return '';
136
90
  })
137
91
  .filter((path) => path !== '');
138
92
  }
139
- /**
140
- * Copies all PNG attachments to the report folder and returns their new names.
141
- */
142
- copyPngAttachments(result, destFolder) {
143
- return result.attachments
144
- .filter((a) => (a.name.endsWith('.png') || (a.contentType || '') === 'image/png') &&
145
- a.name !== 'screenshot')
146
- .map((a) => {
147
- let name = '';
148
- if (a.path) {
149
- name = this.copyToFolder(destFolder, a.path, a.name);
150
- }
151
- else if (a.body) {
152
- const safeName = a.name.endsWith('.png') ? a.name : `${a.name}.png`;
153
- name = this.writeBuffer(destFolder, safeName, a.body);
154
- }
155
- return name ? { path: name, name: a.name } : null;
156
- })
157
- .filter((item) => item !== null);
158
- }
159
93
  /**
160
94
  * Copies all other attachments (traces, logs, etc.) to the report folder.
161
95
  */
@@ -163,7 +97,7 @@ class A11yReportAssets {
163
97
  const excludedNames = ['screenshot', 'video', 'A11y'];
164
98
  return result.attachments
165
99
  .filter((a) => !excludedNames.includes(a.name) &&
166
- !a.name.endsWith('.png') &&
100
+ !a.name.toLowerCase().endsWith('.png') &&
167
101
  !(a.contentType || '').startsWith('image/') &&
168
102
  !(a.contentType || '').startsWith('video/'))
169
103
  .map((a) => {
@@ -172,16 +106,16 @@ class A11yReportAssets {
172
106
  name = this.copyToFolder(destFolder, a.path, a.name);
173
107
  }
174
108
  else if (a.body) {
175
- name = this.writeBuffer(destFolder, a.name, a.body);
109
+ name = this.saveBuffer(destFolder, a.name, a.body);
176
110
  }
177
111
  return name ? { path: name, name: a.name } : null;
178
112
  })
179
113
  .filter((item) => item !== null);
180
114
  }
181
115
  /**
182
- * Writes a buffer to a file in the destination folder.
116
+ * Persists an in-memory buffer to a file in the destination folder.
183
117
  */
184
- writeBuffer(destFolder, fileName, buffer) {
118
+ saveBuffer(destFolder, fileName, buffer) {
185
119
  if (!fs.existsSync(destFolder)) {
186
120
  fs.mkdirSync(destFolder, { recursive: true });
187
121
  }
@@ -1,24 +1,5 @@
1
- import type { Page, Locator, TestInfo } from '@playwright/test';
2
- export interface A11yScannerOptions {
3
- /** Specific selector or locator to include in the scan. */
4
- include?: string | Locator;
5
- /** Alias for include. */
6
- box?: string | Locator;
7
- /** Whether to log violations to the console. @default true */
8
- verbose?: boolean;
9
- /** Alias for verbose. */
10
- consoleLog?: boolean;
11
- /** Specific Axe rules to enable or disable. */
12
- rules?: Record<string, {
13
- enabled: boolean;
14
- }>;
15
- /** Specific WCAG tags to check (e.g., ['wcag2a', 'wcag2aa']). */
16
- tags?: string[];
17
- /** Any other Axe-core options to pass to the builder. */
18
- axeOptions?: Record<string, unknown>;
19
- /** Custom identifier for the report file name. */
20
- pageKey?: string;
21
- }
1
+ import type { Page, TestInfo } from '@playwright/test';
2
+ import { A11yScannerOptions } from './models';
22
3
  /**
23
4
  * Performs an accessibility audit using Axe and Lighthouse.
24
5
  */
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.checkAccessibility = void 0;
7
7
  exports.scanA11y = scanA11y;
8
8
  const playwright_1 = __importDefault(require("@axe-core/playwright"));
9
- const A11yAuditOverlay_1 = require("./A11yAuditOverlay");
10
- const models_1 = require("./models");
9
+ const A11yVisualReporter_1 = require("./A11yVisualReporter");
11
10
  const A11yTimeUtils_1 = require("./A11yTimeUtils");
11
+ const ResolvedColors_1 = require("./ResolvedColors");
12
12
  /**
13
13
  * Sanitizes a string to be safe for use in file paths and prevents path traversal attacks.
14
14
  * Removes or replaces dangerous characters and path separators.
@@ -40,7 +40,7 @@ async function scanA11y(page, testInfo, options = {}) {
40
40
  // Sanitize pageKey to prevent path traversal attacks
41
41
  const rawPageKey = options.pageKey || page.url();
42
42
  const pageKey = sanitizePageKey(rawPageKey);
43
- const overlay = new A11yAuditOverlay_1.A11yAuditOverlay(page);
43
+ const overlay = new A11yVisualReporter_1.A11yVisualReporter(page);
44
44
  // Configure Axe
45
45
  let axeBuilder = new playwright_1.default({ page });
46
46
  const target = options.include || options.box;
@@ -88,10 +88,10 @@ async function scanA11y(page, testInfo, options = {}) {
88
88
  }
89
89
  // Batch log to Browser Console in a single evaluate call
90
90
  if (showBrowser) {
91
- await page.evaluate(([mainMsg, details]) => {
92
- console.log(`%c ${mainMsg}`, 'color: #ea580c; font-weight: bold; font-size: 12px;');
91
+ await page.evaluate(([mainMsg, details, color]) => {
92
+ console.log(`%c ${mainMsg}`, `color: ${color}; font-weight: bold; font-size: 12px;`);
93
93
  details.forEach((msg) => console.log(msg));
94
- }, [mainMsg, detailMessages]);
94
+ }, [mainMsg, detailMessages, ResolvedColors_1.DEFAULT_COLORS.serious]);
95
95
  }
96
96
  }
97
97
  // Fail the test if violations found (softly)
@@ -103,29 +103,23 @@ async function scanA11y(page, testInfo, options = {}) {
103
103
  .toBe(0);
104
104
  // Run Axe Audit
105
105
  const errors = [];
106
- const colorMap = {
107
- minor: '#0ea5e9', // Ocean Blue
108
- moderate: '#f59e0b', // Amber/Honey
109
- serious: '#ea580c', // Deep Orange
110
- critical: '#dc2626', // Power Red
111
- };
112
106
  // Process violations for the report
113
107
  for (const violation of axeResults.violations) {
114
108
  let errorIdx = 0;
115
109
  const targets = [];
116
- const severityColor = colorMap[violation.impact || ''] || '#757575';
110
+ const severityColor = (0, ResolvedColors_1.getSeverityColor)(violation.impact);
117
111
  for (const node of violation.nodes) {
118
112
  for (const selector of node.target) {
119
113
  const elementSelector = selector.toString();
120
114
  const locator = page.locator(elementSelector);
121
- await overlay.showViolationOverlay({ id: violation.id, help: violation.help }, severityColor);
115
+ await overlay.showBanner({ id: violation.id, help: violation.help }, severityColor);
122
116
  if (await locator.isVisible()) {
123
117
  await overlay.highlightElement(elementSelector, severityColor);
124
118
  // Allow a small time for overlay highlight to be visible in video
125
119
  // eslint-disable-next-line playwright/no-wait-for-timeout
126
120
  await page.waitForTimeout(100);
127
121
  const screenshotName = `a11y-${violation.id}-${errorIdx++}.png`;
128
- const buffer = await overlay.captureAndAttachScreenshot(screenshotName, testInfo);
122
+ const buffer = await overlay.captureScreenshot(screenshotName, testInfo);
129
123
  // Capture execution steps for context
130
124
  const excluded = new Set([
131
125
  'Pre Condition',
@@ -147,7 +141,7 @@ async function scanA11y(page, testInfo, options = {}) {
147
141
  stepsJson: JSON.stringify(contextSteps),
148
142
  screenshotBase64: buffer.toString('base64'),
149
143
  });
150
- await overlay.unhighlightElement();
144
+ await overlay.removeHighlight();
151
145
  }
152
146
  }
153
147
  }
@@ -169,16 +163,16 @@ async function scanA11y(page, testInfo, options = {}) {
169
163
  pageUrl: page.url(),
170
164
  accessibilityScore: 0, // No longer used, derivation from Lighthouse removed
171
165
  a11yErrors: errors,
172
- criticalColor: models_1.Severity.critical,
173
- seriousColor: models_1.Severity.serious,
174
- moderateColor: models_1.Severity.moderate,
175
- minorColor: models_1.Severity.minor,
166
+ criticalColor: ResolvedColors_1.DEFAULT_COLORS.critical,
167
+ seriousColor: ResolvedColors_1.DEFAULT_COLORS.serious,
168
+ moderateColor: ResolvedColors_1.DEFAULT_COLORS.moderate,
169
+ minorColor: ResolvedColors_1.DEFAULT_COLORS.minor,
176
170
  adoOrganization: process.env.ADO_ORGANIZATION || '',
177
171
  adoProject: process.env.ADO_PROJECT || '',
178
172
  timestamp: A11yTimeUtils_1.A11yTimeUtils.formatDate(new Date()),
179
173
  };
180
- await overlay.addTestAttachment(testInfo, 'A11y', JSON.stringify(reportData));
181
- await overlay.hideViolationOverlay();
174
+ await overlay.attachData(testInfo, 'A11y', JSON.stringify(reportData));
175
+ await overlay.clean();
182
176
  }
183
177
  /** Alias for backward compatibility */
184
178
  exports.checkAccessibility = scanA11y;
@@ -24,29 +24,17 @@ class A11yTimeUtils {
24
24
  * Formats a Date object into a human-readable string.
25
25
  */
26
26
  static formatDate(date) {
27
- const day = String(date.getDate()).padStart(2, '0');
28
- const monthNames = [
29
- 'Jan',
30
- 'Feb',
31
- 'Mar',
32
- 'Apr',
33
- 'May',
34
- 'Jun',
35
- 'Jul',
36
- 'Aug',
37
- 'Sep',
38
- 'Oct',
39
- 'Nov',
40
- 'Dec',
41
- ];
42
- const month = monthNames[date.getMonth()];
43
- const year = date.getFullYear();
44
- let hours = date.getHours();
45
- const ampm = hours >= 12 ? 'PM' : 'AM';
46
- hours = hours % 12 || 12;
47
- const formattedHours = String(hours).padStart(2, '0');
48
- const minutes = String(date.getMinutes()).padStart(2, '0');
49
- return `${day} ${month} ${year}, ${formattedHours}:${minutes} ${ampm}`;
27
+ const parts = new Intl.DateTimeFormat('en-US', {
28
+ day: '2-digit',
29
+ month: 'short',
30
+ year: 'numeric',
31
+ hour: '2-digit',
32
+ minute: '2-digit',
33
+ hour12: true,
34
+ }).formatToParts(date);
35
+ const get = (type) => { var _a; return ((_a = parts.find((p) => p.type === type)) === null || _a === void 0 ? void 0 : _a.value) || ''; };
36
+ // Standard US Format: MMM DD, YYYY, HH:MM AM/PM
37
+ return `${get('month')} ${get('day')}, ${get('year')}, ${get('hour')}:${get('minute')} ${get('dayPeriod').toUpperCase()}`;
50
38
  }
51
39
  }
52
40
  exports.A11yTimeUtils = A11yTimeUtils;
@@ -0,0 +1,50 @@
1
+ import type { Page, TestInfo } from '@playwright/test';
2
+ /**
3
+ * Manages visual feedback on the page during an accessibility scan.
4
+ * Handles element highlights, violation banners, and report attachments.
5
+ */
6
+ export declare class A11yVisualReporter {
7
+ private readonly page;
8
+ private readonly rootId;
9
+ private static readonly BANNER_ID;
10
+ private static readonly HIGHLIGHT_ID;
11
+ constructor(page: Page);
12
+ /**
13
+ * Shows a violation banner at the bottom of the page.
14
+ */
15
+ showBanner(violation: {
16
+ id: string;
17
+ help: string;
18
+ }, color: string): Promise<void>;
19
+ /**
20
+ * Highlights an element on the page.
21
+ */
22
+ highlightElement(selector: string, color: string): Promise<void>;
23
+ /**
24
+ * Removes all visual feedback from the page.
25
+ */
26
+ clean(): Promise<void>;
27
+ /**
28
+ * Removes the element highlight.
29
+ */
30
+ removeHighlight(): Promise<void>;
31
+ /**
32
+ * Attaches JSON data to the test report.
33
+ */
34
+ attachData(testInfo: TestInfo, name: string, data: string): Promise<void>;
35
+ /**
36
+ * Captures and attaches a screenshot to the test report.
37
+ */
38
+ captureScreenshot(name: string, testInfo: TestInfo): Promise<Buffer>;
39
+ /**
40
+ * Injects helper functions into the page context.
41
+ */
42
+ private ensureHelpers;
43
+ private safeEvaluate;
44
+ }
45
+ declare global {
46
+ interface Window {
47
+ snapAllyGetRoot: (id: string) => ShadowRoot;
48
+ snapAllyToAlpha: (color: string, alpha: number) => string;
49
+ }
50
+ }
@@ -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,24 @@
1
+ export interface AccessibilityReporterOptions {
2
+ /**
3
+ * Folder where the reports will be generated.
4
+ * @default "steps-report"
5
+ */
6
+ outputFolder?: string;
7
+ /**
8
+ * Custom colors for violation severities in the report.
9
+ */
10
+ colors?: {
11
+ critical?: string;
12
+ serious?: string;
13
+ moderate?: string;
14
+ minor?: string;
15
+ };
16
+ /**
17
+ * Azure DevOps integration options.
18
+ */
19
+ ado?: {
20
+ organization?: string;
21
+ project?: string;
22
+ areaPath?: string;
23
+ };
24
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ // ────────────────────────────────────────────────────────────────────────────
3
+ // Public options interface
4
+ // ────────────────────────────────────────────────────────────────────────────
5
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,15 @@
1
+ /** Resolved severity color palette. */
2
+ export interface ResolvedColors {
3
+ critical: string;
4
+ serious: string;
5
+ moderate: string;
6
+ minor: string;
7
+ }
8
+ /** Default severity colors used when the user doesn't override them. */
9
+ export declare const DEFAULT_COLORS: Readonly<ResolvedColors>;
10
+ /** Default fallback color for unknown severities. */
11
+ export declare const FALLBACK_GRAY = "#757575";
12
+ /**
13
+ * Maps an Axe 'impact' level to its corresponding hex color.
14
+ */
15
+ export declare function getSeverityColor(impact?: string | null): string;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FALLBACK_GRAY = exports.DEFAULT_COLORS = void 0;
4
+ exports.getSeverityColor = getSeverityColor;
5
+ /** Default severity colors used when the user doesn't override them. */
6
+ exports.DEFAULT_COLORS = {
7
+ critical: '#dc2626', // Power Red
8
+ serious: '#ea580c', // Deep Orange
9
+ moderate: '#f59e0b', // Amber/Honey
10
+ minor: '#0ea5e9', // Ocean Blue
11
+ };
12
+ /** Default fallback color for unknown severities. */
13
+ exports.FALLBACK_GRAY = '#757575';
14
+ /**
15
+ * Maps an Axe 'impact' level to its corresponding hex color.
16
+ */
17
+ function getSeverityColor(impact) {
18
+ const level = (impact || 'minor');
19
+ return exports.DEFAULT_COLORS[level] || exports.FALLBACK_GRAY;
20
+ }