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.
- package/dist/A11yReportAssets.d.ts +5 -12
- package/dist/A11yReportAssets.js +16 -82
- package/dist/A11yScanner.d.ts +2 -21
- package/dist/A11yScanner.js +16 -22
- package/dist/A11yTimeUtils.js +11 -23
- package/dist/A11yVisualReporter.d.ts +50 -0
- package/dist/A11yVisualReporter.js +188 -0
- package/dist/AccessibilityReporterOptions.d.ts +24 -0
- package/dist/AccessibilityReporterOptions.js +5 -0
- package/dist/ResolvedColors.d.ts +15 -0
- package/dist/ResolvedColors.js +20 -0
- package/dist/SnapAllyReporter.d.ts +12 -72
- package/dist/SnapAllyReporter.js +218 -329
- package/dist/core/A11yHtmlRenderer.d.ts +17 -0
- package/dist/core/A11yHtmlRenderer.js +118 -0
- package/dist/core/A11yReportAssets.d.ts +30 -0
- package/dist/core/A11yReportAssets.js +127 -0
- package/dist/core/A11yScanner.d.ts +8 -0
- package/dist/core/A11yScanner.js +178 -0
- package/dist/core/A11yVisualReporter.d.ts +50 -0
- package/dist/core/A11yVisualReporter.js +188 -0
- package/dist/core/HtmlRenderer.d.ts +14 -0
- package/dist/core/HtmlRenderer.js +106 -0
- package/dist/core/ReportAssets.d.ts +29 -0
- package/dist/core/ReportAssets.js +126 -0
- package/dist/core/Scanner.d.ts +7 -0
- package/dist/core/Scanner.js +162 -0
- package/dist/core/VisualReporter.d.ts +54 -0
- package/dist/core/VisualReporter.js +192 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +13 -12
- package/dist/models/A11yDataSource.d.ts +15 -0
- package/dist/models/A11yDataSource.js +2 -0
- package/dist/models/A11yError.d.ts +34 -0
- package/dist/models/A11yError.js +11 -0
- package/dist/models/A11yScannerOptions.d.ts +24 -0
- package/dist/models/A11yScannerOptions.js +2 -0
- package/dist/models/AccessibilityReporterOptions.d.ts +24 -0
- package/dist/models/AccessibilityReporterOptions.js +5 -0
- package/dist/models/DataSource.d.ts +15 -0
- package/dist/models/DataSource.js +2 -0
- package/dist/models/ImagePath.d.ts +5 -0
- package/dist/models/ImagePath.js +3 -0
- package/dist/models/ReportData.d.ts +24 -0
- package/dist/models/ReportData.js +2 -0
- package/dist/models/ReporterOptions.d.ts +24 -0
- package/dist/models/ReporterOptions.js +5 -0
- package/dist/models/ResolvedColors.d.ts +16 -0
- package/dist/models/ResolvedColors.js +24 -0
- package/dist/models/ScannerOptions.d.ts +30 -0
- package/dist/models/ScannerOptions.js +2 -0
- package/dist/models/Severity.d.ts +7 -0
- package/dist/models/Severity.js +11 -0
- package/dist/models/Target.d.ts +10 -0
- package/dist/models/Target.js +3 -0
- package/dist/models/TestResults.d.ts +41 -0
- package/dist/models/TestResults.js +2 -0
- package/dist/models/TestStatusIcon.d.ts +8 -0
- package/dist/models/TestStatusIcon.js +12 -0
- package/dist/models/TestSummary.d.ts +34 -0
- package/dist/models/TestSummary.js +2 -0
- package/dist/models/Violation.d.ts +13 -0
- package/dist/models/Violation.js +2 -0
- package/dist/models/index.d.ts +12 -113
- package/dist/models/index.js +26 -16
- package/dist/templates/accessibility-report.html +75 -101
- package/dist/templates/execution-summary.html +37 -103
- package/dist/templates/global-report-styles.css +400 -9
- package/dist/templates/report-app.js +171 -73
- package/dist/templates/test-execution-report.html +84 -113
- package/dist/utils/A11yTimeUtils.d.ts +13 -0
- package/dist/utils/A11yTimeUtils.js +40 -0
- package/dist/utils/TimeUtils.d.ts +13 -0
- package/dist/utils/TimeUtils.js +39 -0
- 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
|
|
12
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
27
|
+
* Persists an in-memory buffer to a file in the destination folder.
|
|
35
28
|
*/
|
|
36
|
-
|
|
29
|
+
saveBuffer(destFolder: string, fileName: string, buffer: Buffer): string;
|
|
37
30
|
}
|
package/dist/A11yReportAssets.js
CHANGED
|
@@ -56,72 +56,26 @@ class A11yReportAssets {
|
|
|
56
56
|
return name;
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* Copies the
|
|
60
|
-
*
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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' ||
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
116
|
+
* Persists an in-memory buffer to a file in the destination folder.
|
|
183
117
|
*/
|
|
184
|
-
|
|
118
|
+
saveBuffer(destFolder, fileName, buffer) {
|
|
185
119
|
if (!fs.existsSync(destFolder)) {
|
|
186
120
|
fs.mkdirSync(destFolder, { recursive: true });
|
|
187
121
|
}
|
package/dist/A11yScanner.d.ts
CHANGED
|
@@ -1,24 +1,5 @@
|
|
|
1
|
-
import type { Page,
|
|
2
|
-
|
|
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
|
*/
|
package/dist/A11yScanner.js
CHANGED
|
@@ -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
|
|
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
|
|
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}`,
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
173
|
-
seriousColor:
|
|
174
|
-
moderateColor:
|
|
175
|
-
minorColor:
|
|
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.
|
|
181
|
-
await overlay.
|
|
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;
|
package/dist/A11yTimeUtils.js
CHANGED
|
@@ -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
|
|
28
|
-
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
}
|