snap-ally 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -12
- package/dist/SnapAllyReporter.js +12 -19
- package/dist/core/Scanner.js +16 -14
- package/dist/models/ReporterOptions.d.ts +10 -0
- package/dist/models/ResolvedColors.js +4 -4
- package/dist/templates/report-app.js +4 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/snap-ally)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://ko-fi.com/U7U1U2V3V)
|
|
5
6
|
|
|
6
7
|
A powerful, developer-friendly Playwright reporter for **Accessibility testing** using Axe-core. Beyond just reporting, it provides visual evidence to help developers fix accessibility issues faster.
|
|
7
8
|
|
|
@@ -60,15 +61,18 @@ export default defineConfig({
|
|
|
60
61
|
outputFolder: 'a11y-report',
|
|
61
62
|
// Optional: Visual Customization
|
|
62
63
|
colors: {
|
|
63
|
-
critical: '#
|
|
64
|
-
serious: '#
|
|
65
|
-
moderate: '#
|
|
66
|
-
minor: '#
|
|
64
|
+
critical: '#b91c1c',
|
|
65
|
+
serious: '#c2410c',
|
|
66
|
+
moderate: '#a16207',
|
|
67
|
+
minor: '#1e40af',
|
|
67
68
|
},
|
|
69
|
+
verbose: true, // Show in terminal
|
|
70
|
+
consoleLog: true, // Show in browser console
|
|
68
71
|
// Optional: Azure DevOps Integration
|
|
69
72
|
ado: {
|
|
70
73
|
organization: 'your-org',
|
|
71
74
|
project: 'your-project',
|
|
75
|
+
areaPath: 'your-project\\your-team', // Optional: Define where bugs should be created
|
|
72
76
|
},
|
|
73
77
|
},
|
|
74
78
|
],
|
|
@@ -80,20 +84,20 @@ export default defineConfig({
|
|
|
80
84
|
|
|
81
85
|
## <span aria-hidden="true">📖</span> Usage
|
|
82
86
|
|
|
83
|
-
Import and use `
|
|
87
|
+
Import and use `checkAccessibility` within your Playwright tests:
|
|
84
88
|
|
|
85
89
|
```typescript
|
|
86
90
|
import { test } from '@playwright/test';
|
|
87
|
-
import {
|
|
91
|
+
import { checkAccessibility } from 'snap-ally';
|
|
88
92
|
|
|
89
93
|
test('verify page accessibility', async ({ page }, testInfo) => {
|
|
90
94
|
await page.goto('https://example.com');
|
|
91
95
|
|
|
92
96
|
// Basic scan
|
|
93
|
-
await
|
|
97
|
+
await checkAccessibility(page, testInfo);
|
|
94
98
|
|
|
95
99
|
// Advanced scan with configuration
|
|
96
|
-
await
|
|
100
|
+
await checkAccessibility(page, testInfo, {
|
|
97
101
|
verbose: true, // Log results to terminal
|
|
98
102
|
consoleLog: true, // Log results to browser console
|
|
99
103
|
pageKey: 'Homepage', // Custom name for the report file
|
|
@@ -113,13 +117,16 @@ test('verify page accessibility', async ({ page }, testInfo) => {
|
|
|
113
117
|
|
|
114
118
|
| Option | Type | Description |
|
|
115
119
|
| ------------------ | -------- | --------------------------------------------------------------- |
|
|
116
|
-
| `outputFolder` | `string`
|
|
117
|
-
| `colors` | `object`
|
|
118
|
-
| `
|
|
120
|
+
| `outputFolder` | `string` | Where to save the reports. Defaults to `steps-report`. |
|
|
121
|
+
| `colors` | `object` | Customize severity colors (critical, serious, moderate, minor). |
|
|
122
|
+
| `verbose` | `boolean` | **Default**: `true`. Show violations in terminal. |
|
|
123
|
+
| `consoleLog` | `boolean` | **Default**: `true`. Show violations in browser console. |
|
|
124
|
+
| `ado` | `object` | Azure DevOps configuration for deep linking. |
|
|
119
125
|
| `ado.organization` | `string` | Your Azure DevOps organization name. |
|
|
120
126
|
| `ado.project` | `string` | Your Azure DevOps project name. |
|
|
127
|
+
| `ado.areaPath` | `string` | Optional: The Area Path where bugs should be created. |
|
|
121
128
|
|
|
122
|
-
### `
|
|
129
|
+
### `checkAccessibility` Options
|
|
123
130
|
|
|
124
131
|
| Option | Type | Description |
|
|
125
132
|
| ------------ | ---------- | -------------------------------------------------------------------------- |
|
package/dist/SnapAllyReporter.js
CHANGED
|
@@ -68,7 +68,7 @@ class SnapAllyReporter {
|
|
|
68
68
|
};
|
|
69
69
|
this.testRuleCounts = {};
|
|
70
70
|
this.testGlobalCounts = {};
|
|
71
|
-
this.options = options;
|
|
71
|
+
this.options = { verbose: true, consoleLog: true, ...options };
|
|
72
72
|
this.outputFolder = path.resolve(process.cwd(), options.outputFolder || 'steps-report');
|
|
73
73
|
this.validateOutputFolder(this.outputFolder);
|
|
74
74
|
this.colors = {
|
|
@@ -92,23 +92,23 @@ class SnapAllyReporter {
|
|
|
92
92
|
const normalizedCwd = path.normalize(cwd);
|
|
93
93
|
// Prevent deletion of current working directory
|
|
94
94
|
if (normalizedPath === normalizedCwd) {
|
|
95
|
-
throw new Error(
|
|
95
|
+
throw new Error('[SnapAlly] Invalid outputFolder: Cannot delete the current working directory. ' +
|
|
96
96
|
`Resolved path: "${resolvedPath}"`);
|
|
97
97
|
}
|
|
98
98
|
// Prevent deletion of parent directories
|
|
99
99
|
if (normalizedCwd.startsWith(normalizedPath + path.sep) || normalizedCwd.startsWith(normalizedPath + '/')) {
|
|
100
|
-
throw new Error(
|
|
100
|
+
throw new Error('[SnapAlly] Invalid outputFolder: Cannot delete a parent directory of the current working directory. ' +
|
|
101
101
|
`Resolved path: "${resolvedPath}"`);
|
|
102
102
|
}
|
|
103
103
|
// Prevent deletion of root or near-root directories
|
|
104
104
|
const pathSegments = normalizedPath.split(path.sep).filter(s => s.length > 0);
|
|
105
105
|
if (pathSegments.length <= 1) {
|
|
106
|
-
throw new Error(
|
|
106
|
+
throw new Error('[SnapAlly] Invalid outputFolder: Path is too close to root directory. ' +
|
|
107
107
|
`Resolved path: "${resolvedPath}"`);
|
|
108
108
|
}
|
|
109
109
|
// Ensure the path is within the current working directory (safest approach)
|
|
110
110
|
if (!normalizedPath.startsWith(normalizedCwd + path.sep) && !normalizedPath.startsWith(normalizedCwd + '/')) {
|
|
111
|
-
throw new Error(
|
|
111
|
+
throw new Error('[SnapAlly] Invalid outputFolder: Path must be within the current working directory. ' +
|
|
112
112
|
`Resolved path: "${resolvedPath}", CWD: "${cwd}"`);
|
|
113
113
|
}
|
|
114
114
|
}
|
|
@@ -134,17 +134,14 @@ class SnapAllyReporter {
|
|
|
134
134
|
console.log(`\n[SnapAlly] Report generated: ${summaryPath}`);
|
|
135
135
|
}
|
|
136
136
|
async processTestResult(test, result, index) {
|
|
137
|
-
var _a, _b, _c
|
|
137
|
+
var _a, _b, _c;
|
|
138
138
|
const testFolderName = `test-${index}`;
|
|
139
139
|
const testFolder = path.join(this.outputFolder, testFolderName);
|
|
140
140
|
const videoPath = this.assetsManager.copyVideos(result, testFolder);
|
|
141
141
|
const screenshotPaths = this.assetsManager.copyScreenshots(result, testFolder);
|
|
142
142
|
const attachments = this.assetsManager.copyAllOtherAttachments(result, testFolder);
|
|
143
143
|
const a11yAttachment = result.attachments.find((a) => a.name === 'A11y');
|
|
144
|
-
if (a11yAttachment) {
|
|
145
|
-
console.log(`[SnapAlly] Found A11y attachment for project: ${test._projectId}`);
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
144
|
+
if (!a11yAttachment && this.options.verbose) {
|
|
148
145
|
console.warn(`[SnapAlly] A11y attachment missing for test: ${test.title}. Available: ${result.attachments.map(a => a.name).join(', ')}`);
|
|
149
146
|
}
|
|
150
147
|
let a11yData = null;
|
|
@@ -157,16 +154,14 @@ class SnapAllyReporter {
|
|
|
157
154
|
}
|
|
158
155
|
}
|
|
159
156
|
// Handle cases where a11yData might be the direct ReportData or wrapped in a data property
|
|
160
|
-
const actualData = (a11yData
|
|
157
|
+
const actualData = (a11yData && typeof a11yData === 'object' && 'data' in a11yData ? a11yData.data : a11yData);
|
|
161
158
|
const violations = (actualData === null || actualData === void 0 ? void 0 : actualData.a11yErrors) || (actualData === null || actualData === void 0 ? void 0 : actualData.violations) || [];
|
|
162
159
|
const a11yErrorCount = violations.reduce((acc, curr) => { var _a, _b; return acc + (curr.total || ((_a = curr.target) === null || _a === void 0 ? void 0 : _a.length) || ((_b = curr.nodes) === null || _b === void 0 ? void 0 : _b.length) || 0); }, 0);
|
|
163
160
|
const filteredSteps = (() => {
|
|
164
|
-
const sRaw = result.steps.map(s => s.title);
|
|
165
161
|
const blocklist = ['Evaluate', 'Create page', 'Close page', 'Before Hooks', 'After Hooks', 'Worker Teardown', 'Worker Cleanup', 'Attach', 'Wait for timeout', 'Capture A11y screenshot'];
|
|
166
162
|
const filtered = result.steps
|
|
167
163
|
.filter((s) => !blocklist.some(b => s.title.includes(b)))
|
|
168
164
|
.map((s) => s.title);
|
|
169
|
-
console.log(`[SnapAlly] Steps for ${test.title}: Raw=${sRaw.length}, Filtered=${filtered.length}`);
|
|
170
165
|
return filtered;
|
|
171
166
|
})();
|
|
172
167
|
const testResults = {
|
|
@@ -181,13 +176,11 @@ class SnapAllyReporter {
|
|
|
181
176
|
statusIcon: this.getStatusIcon(result.status),
|
|
182
177
|
browser: (() => {
|
|
183
178
|
var _a, _b;
|
|
184
|
-
if (test.outcome() === 'skipped')
|
|
185
|
-
return 'n/a';
|
|
186
179
|
const bName = test._projectId ||
|
|
187
180
|
((_b = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project()) === null || _b === void 0 ? void 0 : _b.name) ||
|
|
188
181
|
test.projectName ||
|
|
189
182
|
'chromium';
|
|
190
|
-
return bName;
|
|
183
|
+
return test.outcome() === 'skipped' ? 'n/a' : bName;
|
|
191
184
|
})(),
|
|
192
185
|
adoOrganization: ((_a = this.options.ado) === null || _a === void 0 ? void 0 : _a.organization) || (actualData === null || actualData === void 0 ? void 0 : actualData.adoOrganization),
|
|
193
186
|
adoProject: ((_b = this.options.ado) === null || _b === void 0 ? void 0 : _b.project) || (actualData === null || actualData === void 0 ? void 0 : actualData.adoProject),
|
|
@@ -222,7 +215,7 @@ class SnapAllyReporter {
|
|
|
222
215
|
console.log(`[SnapAlly] Generating A11y report for ${test.title} (Browser: ${testResults.browser})`);
|
|
223
216
|
await this.renderer.render('accessibility-report.html', testResults, testFolder, a11yReportPath);
|
|
224
217
|
}
|
|
225
|
-
|
|
218
|
+
// Removed debug logging of internal data state
|
|
226
219
|
const reportPath = path.join(testFolder, reportFileName);
|
|
227
220
|
await this.renderer.render('test-execution-report.html', testResults, testFolder, reportPath);
|
|
228
221
|
this.updateSummary(test, testResults);
|
|
@@ -299,7 +292,7 @@ class SnapAllyReporter {
|
|
|
299
292
|
if (!this.executionSummary.wcagErrors[ruleId]) {
|
|
300
293
|
this.executionSummary.wcagErrors[ruleId] = {
|
|
301
294
|
count: 0,
|
|
302
|
-
severity: err.severity || err.impact,
|
|
295
|
+
severity: err.severity || err.impact || 'minor',
|
|
303
296
|
helpUrl: err.helpUrl,
|
|
304
297
|
description: err.description,
|
|
305
298
|
};
|
|
@@ -315,7 +308,7 @@ class SnapAllyReporter {
|
|
|
315
308
|
if (!bSummary.wcagErrors[ruleId]) {
|
|
316
309
|
bSummary.wcagErrors[ruleId] = {
|
|
317
310
|
count: 0,
|
|
318
|
-
severity: err.severity || err.impact,
|
|
311
|
+
severity: err.severity || err.impact || 'minor',
|
|
319
312
|
helpUrl: err.helpUrl,
|
|
320
313
|
description: err.description,
|
|
321
314
|
};
|
package/dist/core/Scanner.js
CHANGED
|
@@ -27,9 +27,15 @@ function sanitizePageKey(input) {
|
|
|
27
27
|
* Performs an accessibility audit using Axe and Lighthouse.
|
|
28
28
|
*/
|
|
29
29
|
async function scanA11y(page, testInfo, options = {}) {
|
|
30
|
-
var _a, _b, _c, _d, _e, _f;
|
|
31
|
-
|
|
32
|
-
const
|
|
30
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
31
|
+
// 1. Find reporter config for global defaults
|
|
32
|
+
const reporterConfig = testInfo.config.reporter.find((r) => Array.isArray(r) &&
|
|
33
|
+
(typeof r[0] === 'string' &&
|
|
34
|
+
(r[0].includes('SnapAllyReporter') || r[0].endsWith('SnapAllyReporter.ts'))));
|
|
35
|
+
const globalOptions = (Array.isArray(reporterConfig) ? ((_a = reporterConfig[1]) !== null && _a !== void 0 ? _a : {}) : {});
|
|
36
|
+
// 2. Resolve final options (local > global > default)
|
|
37
|
+
const showTerminal = (_c = (_b = options.verbose) !== null && _b !== void 0 ? _b : globalOptions.verbose) !== null && _c !== void 0 ? _c : true;
|
|
38
|
+
const showBrowser = (_e = (_d = options.consoleLog) !== null && _d !== void 0 ? _d : globalOptions.consoleLog) !== null && _e !== void 0 ? _e : true;
|
|
33
39
|
const rawPageKey = options.pageKey || page.url();
|
|
34
40
|
const pageKey = sanitizePageKey(rawPageKey);
|
|
35
41
|
const overlay = new VisualReporter_1.VisualReporter(page);
|
|
@@ -82,14 +88,10 @@ async function scanA11y(page, testInfo, options = {}) {
|
|
|
82
88
|
}, [mainMsg, detailMessages, models_1.DEFAULT_COLORS.serious]);
|
|
83
89
|
}
|
|
84
90
|
}
|
|
85
|
-
test_1.
|
|
86
|
-
.soft(violationCount
|
|
87
|
-
|
|
88
|
-
const
|
|
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;
|
|
91
|
+
await test_1.test.step('Check Accessibility', async () => {
|
|
92
|
+
test_1.expect.soft(violationCount).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
const customColors = globalOptions === null || globalOptions === void 0 ? void 0 : globalOptions.colors;
|
|
93
95
|
const errors = [];
|
|
94
96
|
for (const violation of axeResults.violations) {
|
|
95
97
|
let errorIdx = 0;
|
|
@@ -151,9 +153,9 @@ async function scanA11y(page, testInfo, options = {}) {
|
|
|
151
153
|
seriousColor: (customColors === null || customColors === void 0 ? void 0 : customColors.serious) || models_1.DEFAULT_COLORS.serious,
|
|
152
154
|
moderateColor: (customColors === null || customColors === void 0 ? void 0 : customColors.moderate) || models_1.DEFAULT_COLORS.moderate,
|
|
153
155
|
minorColor: (customColors === null || customColors === void 0 ? void 0 : customColors.minor) || models_1.DEFAULT_COLORS.minor,
|
|
154
|
-
adoOrganization: ((
|
|
155
|
-
adoProject: ((
|
|
156
|
-
adoAreaPath: ((
|
|
156
|
+
adoOrganization: ((_f = options.ado) === null || _f === void 0 ? void 0 : _f.organization) || process.env.ADO_ORGANIZATION || '',
|
|
157
|
+
adoProject: ((_g = options.ado) === null || _g === void 0 ? void 0 : _g.project) || process.env.ADO_PROJECT || '',
|
|
158
|
+
adoAreaPath: ((_h = options.ado) === null || _h === void 0 ? void 0 : _h.areaPath) || process.env.ADO_AREA_PATH || '',
|
|
157
159
|
timestamp: TimeUtils_1.TimeUtils.formatDate(new Date()),
|
|
158
160
|
};
|
|
159
161
|
await overlay.attachJsonData(testInfo, 'A11y', JSON.stringify(reportData));
|
|
@@ -13,6 +13,16 @@ export interface ReporterOptions {
|
|
|
13
13
|
moderate?: string;
|
|
14
14
|
minor?: string;
|
|
15
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* Whether to log violations to the terminal.
|
|
18
|
+
* @default true
|
|
19
|
+
*/
|
|
20
|
+
verbose?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether to log violations to the browser console.
|
|
23
|
+
* @default true
|
|
24
|
+
*/
|
|
25
|
+
consoleLog?: boolean;
|
|
16
26
|
/**
|
|
17
27
|
* Azure DevOps integration options.
|
|
18
28
|
*/
|
|
@@ -4,10 +4,10 @@ exports.FALLBACK_GRAY = exports.DEFAULT_COLORS = void 0;
|
|
|
4
4
|
exports.getSeverityColor = getSeverityColor;
|
|
5
5
|
/** Default severity colors used when the user doesn't override them. */
|
|
6
6
|
exports.DEFAULT_COLORS = {
|
|
7
|
-
critical: '#
|
|
8
|
-
serious: '#
|
|
9
|
-
moderate: '#
|
|
10
|
-
minor: '#
|
|
7
|
+
critical: '#b91c1c', // Deep Red
|
|
8
|
+
serious: '#c2410c', // Deep Orange
|
|
9
|
+
moderate: '#a16207', // Dark Amber
|
|
10
|
+
minor: '#1e40af', // Royal Blue
|
|
11
11
|
};
|
|
12
12
|
/** Default fallback color for unknown severities. */
|
|
13
13
|
exports.FALLBACK_GRAY = '#757575';
|
|
@@ -711,8 +711,10 @@ function renderBarChart(elementId, wcagData, colors) {
|
|
|
711
711
|
|
|
712
712
|
const chartColors = wcagEntries.map((e) => (colors && colors[e[1].severity]) || '#ef4444');
|
|
713
713
|
|
|
714
|
+
const chartHeight = 80 + (wcagEntries.length * 50);
|
|
715
|
+
|
|
714
716
|
new ApexCharts(el, {
|
|
715
|
-
chart: { type: 'bar', height:
|
|
717
|
+
chart: { type: 'bar', height: chartHeight, toolbar: { show: false } },
|
|
716
718
|
series: [{ name: 'Violations', data: wcagEntries.map((e) => e[1].count) }],
|
|
717
719
|
xaxis: {
|
|
718
720
|
categories: wcagEntries.map((e) => e[0]),
|
|
@@ -733,7 +735,7 @@ function renderBarChart(elementId, wcagData, colors) {
|
|
|
733
735
|
bar: {
|
|
734
736
|
borderRadius: 6,
|
|
735
737
|
horizontal: true,
|
|
736
|
-
barHeight: '
|
|
738
|
+
barHeight: wcagEntries.length === 1 ? '35%' : wcagEntries.length <= 3 ? '50%' : '75%',
|
|
737
739
|
distributed: true,
|
|
738
740
|
dataLabels: { position: 'top' },
|
|
739
741
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snap-ally",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "A custom Playwright reporter for Accessibility testing using Axe, with HTML reporting and Azure DevOps integration.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -48,4 +48,4 @@
|
|
|
48
48
|
"prettier": "^3.8.1",
|
|
49
49
|
"typescript": "^5.0.0"
|
|
50
50
|
}
|
|
51
|
-
}
|
|
51
|
+
}
|