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.
- 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 +62 -95
- package/dist/templates/execution-summary.html +37 -103
- package/dist/templates/global-report-styles.css +400 -9
- package/dist/templates/report-app.js +170 -72
- package/dist/templates/test-execution-report.html +84 -121
- 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
package/dist/SnapAllyReporter.js
CHANGED
|
@@ -33,45 +33,23 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
const models_1 = require("./models");
|
|
37
|
-
const A11yReportAssets_1 = require("./A11yReportAssets");
|
|
38
|
-
const A11yHtmlRenderer_1 = require("./A11yHtmlRenderer");
|
|
39
|
-
const A11yTimeUtils_1 = require("./A11yTimeUtils");
|
|
40
|
-
const path = __importStar(require("path"));
|
|
41
36
|
const fs = __importStar(require("fs"));
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
minor: '#0891b2',
|
|
48
|
-
};
|
|
49
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
50
|
-
// Reporter
|
|
51
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const HtmlRenderer_1 = require("./core/HtmlRenderer");
|
|
39
|
+
const ReportAssets_1 = require("./core/ReportAssets");
|
|
40
|
+
const TimeUtils_1 = require("./utils/TimeUtils");
|
|
41
|
+
const models_1 = require("./models");
|
|
52
42
|
/**
|
|
53
43
|
* Playwright reporter for accessibility audits and test steps.
|
|
54
|
-
*
|
|
55
|
-
* Generates:
|
|
56
|
-
* - A per-test execution report (steps, video, screenshots, errors)
|
|
57
|
-
* - A per-scan accessibility report (violations, evidence, ADO integration)
|
|
58
|
-
* - A global execution summary with per-browser breakdowns
|
|
59
44
|
*/
|
|
60
45
|
class SnapAllyReporter {
|
|
61
46
|
constructor(options = {}) {
|
|
62
47
|
var _a, _b, _c, _d;
|
|
63
|
-
this.assetsManager = new
|
|
64
|
-
this.renderer = new
|
|
48
|
+
this.assetsManager = new ReportAssets_1.ReportAssets();
|
|
49
|
+
this.renderer = new HtmlRenderer_1.HtmlRenderer();
|
|
65
50
|
this.projectRoot = 'tests';
|
|
66
|
-
/**
|
|
67
|
-
* Monotonically increasing test counter.
|
|
68
|
-
* Incremented synchronously in {@link onTestEnd} to avoid race conditions
|
|
69
|
-
* when multiple async {@link processTestResult} calls run concurrently.
|
|
70
|
-
*/
|
|
71
51
|
this.testIndex = 0;
|
|
72
|
-
/** Async tasks queued by `onTestEnd`; drained in `onEnd`. */
|
|
73
52
|
this.tasks = [];
|
|
74
|
-
/** Aggregated data for the final summary report. */
|
|
75
53
|
this.executionSummary = {
|
|
76
54
|
duration: '',
|
|
77
55
|
status: '',
|
|
@@ -86,341 +64,196 @@ class SnapAllyReporter {
|
|
|
86
64
|
totalA11yErrorCount: 0,
|
|
87
65
|
browserSummaries: {},
|
|
88
66
|
date: '',
|
|
67
|
+
colors: {},
|
|
89
68
|
};
|
|
69
|
+
this.testRuleCounts = {};
|
|
70
|
+
this.testGlobalCounts = {};
|
|
90
71
|
this.options = options;
|
|
91
72
|
this.outputFolder = path.resolve(process.cwd(), options.outputFolder || 'steps-report');
|
|
73
|
+
this.validateOutputFolder(this.outputFolder);
|
|
92
74
|
this.colors = {
|
|
93
|
-
critical: ((_a = options.colors) === null || _a === void 0 ? void 0 : _a.critical) || DEFAULT_COLORS.critical,
|
|
94
|
-
serious: ((_b = options.colors) === null || _b === void 0 ? void 0 : _b.serious) || DEFAULT_COLORS.serious,
|
|
95
|
-
moderate: ((_c = options.colors) === null || _c === void 0 ? void 0 : _c.moderate) || DEFAULT_COLORS.moderate,
|
|
96
|
-
minor: ((_d = options.colors) === null || _d === void 0 ? void 0 : _d.minor) || DEFAULT_COLORS.minor,
|
|
75
|
+
critical: ((_a = options.colors) === null || _a === void 0 ? void 0 : _a.critical) || models_1.DEFAULT_COLORS.critical,
|
|
76
|
+
serious: ((_b = options.colors) === null || _b === void 0 ? void 0 : _b.serious) || models_1.DEFAULT_COLORS.serious,
|
|
77
|
+
moderate: ((_c = options.colors) === null || _c === void 0 ? void 0 : _c.moderate) || models_1.DEFAULT_COLORS.moderate,
|
|
78
|
+
minor: ((_d = options.colors) === null || _d === void 0 ? void 0 : _d.minor) || models_1.DEFAULT_COLORS.minor,
|
|
97
79
|
};
|
|
80
|
+
this.executionSummary.colors = this.colors;
|
|
98
81
|
}
|
|
99
82
|
printsToStdio() {
|
|
100
|
-
return
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Validates that the output folder is safe to delete.
|
|
87
|
+
* Prevents accidental deletion of critical directories like repo root, parent dirs, or system paths.
|
|
88
|
+
*/
|
|
89
|
+
validateOutputFolder(resolvedPath) {
|
|
90
|
+
const cwd = process.cwd();
|
|
91
|
+
const normalizedPath = path.normalize(resolvedPath);
|
|
92
|
+
const normalizedCwd = path.normalize(cwd);
|
|
93
|
+
// Prevent deletion of current working directory
|
|
94
|
+
if (normalizedPath === normalizedCwd) {
|
|
95
|
+
throw new Error(`[SnapAlly] Invalid outputFolder: Cannot delete the current working directory. ` +
|
|
96
|
+
`Resolved path: "${resolvedPath}"`);
|
|
97
|
+
}
|
|
98
|
+
// Prevent deletion of parent directories
|
|
99
|
+
if (normalizedCwd.startsWith(normalizedPath + path.sep) || normalizedCwd.startsWith(normalizedPath + '/')) {
|
|
100
|
+
throw new Error(`[SnapAlly] Invalid outputFolder: Cannot delete a parent directory of the current working directory. ` +
|
|
101
|
+
`Resolved path: "${resolvedPath}"`);
|
|
102
|
+
}
|
|
103
|
+
// Prevent deletion of root or near-root directories
|
|
104
|
+
const pathSegments = normalizedPath.split(path.sep).filter(s => s.length > 0);
|
|
105
|
+
if (pathSegments.length <= 1) {
|
|
106
|
+
throw new Error(`[SnapAlly] Invalid outputFolder: Path is too close to root directory. ` +
|
|
107
|
+
`Resolved path: "${resolvedPath}"`);
|
|
108
|
+
}
|
|
109
|
+
// Ensure the path is within the current working directory (safest approach)
|
|
110
|
+
if (!normalizedPath.startsWith(normalizedCwd + path.sep) && !normalizedPath.startsWith(normalizedCwd + '/')) {
|
|
111
|
+
throw new Error(`[SnapAlly] Invalid outputFolder: Path must be within the current working directory. ` +
|
|
112
|
+
`Resolved path: "${resolvedPath}", CWD: "${cwd}"`);
|
|
113
|
+
}
|
|
101
114
|
}
|
|
102
115
|
onBegin(config) {
|
|
103
|
-
this.projectRoot = config.rootDir
|
|
116
|
+
this.projectRoot = config.rootDir;
|
|
117
|
+
if (fs.existsSync(this.outputFolder)) {
|
|
118
|
+
fs.rmSync(this.outputFolder, { recursive: true, force: true });
|
|
119
|
+
}
|
|
104
120
|
}
|
|
105
121
|
onTestEnd(test, result) {
|
|
106
|
-
// Increment index synchronously so concurrent tasks never share an index.
|
|
107
122
|
const index = ++this.testIndex;
|
|
108
123
|
this.tasks.push(this.processTestResult(test, result, index));
|
|
109
124
|
}
|
|
110
125
|
async onEnd(result) {
|
|
111
126
|
await Promise.all(this.tasks);
|
|
112
|
-
|
|
127
|
+
const summaryPath = path.join(this.outputFolder, 'summary.html');
|
|
113
128
|
this.executionSummary.status = result.status;
|
|
114
129
|
this.executionSummary.statusIcon =
|
|
115
|
-
|
|
116
|
-
this.executionSummary.date =
|
|
117
|
-
|
|
118
|
-
await this.renderer.render('execution-summary.html',
|
|
119
|
-
console.log(`\n[SnapAlly]
|
|
130
|
+
result.status === 'passed' ? models_1.TestStatusIcon.passed : models_1.TestStatusIcon.failed;
|
|
131
|
+
this.executionSummary.date = TimeUtils_1.TimeUtils.formatDate(new Date());
|
|
132
|
+
this.executionSummary.duration = TimeUtils_1.TimeUtils.formatDuration(result.duration);
|
|
133
|
+
await this.renderer.render('execution-summary.html', this.executionSummary, this.outputFolder, summaryPath);
|
|
134
|
+
console.log(`\n[SnapAlly] Report generated: ${summaryPath}`);
|
|
120
135
|
}
|
|
121
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
122
|
-
// Core per-test processing
|
|
123
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
124
136
|
async processTestResult(test, result, index) {
|
|
125
|
-
|
|
126
|
-
const testFolderName =
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
this.
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
137
|
+
var _a, _b, _c, _d;
|
|
138
|
+
const testFolderName = `test-${index}`;
|
|
139
|
+
const testFolder = path.join(this.outputFolder, testFolderName);
|
|
140
|
+
const videoPath = this.assetsManager.copyVideos(result, testFolder);
|
|
141
|
+
const screenshotPaths = this.assetsManager.copyScreenshots(result, testFolder);
|
|
142
|
+
const attachments = this.assetsManager.copyAllOtherAttachments(result, testFolder);
|
|
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 {
|
|
148
|
+
console.warn(`[SnapAlly] A11y attachment missing for test: ${test.title}. Available: ${result.attachments.map(a => a.name).join(', ')}`);
|
|
149
|
+
}
|
|
150
|
+
let a11yData = null;
|
|
151
|
+
if (a11yAttachment && a11yAttachment.body) {
|
|
152
|
+
try {
|
|
153
|
+
a11yData = JSON.parse(a11yAttachment.body.toString());
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
console.error(`[SnapAlly] Failed to parse A11y attachment: ${err}. Body was: ${a11yAttachment.body.toString().substring(0, 100)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Handle cases where a11yData might be the direct ReportData or wrapped in a data property
|
|
160
|
+
const actualData = (a11yData === null || a11yData === void 0 ? void 0 : a11yData.data) || a11yData;
|
|
161
|
+
const violations = (actualData === null || actualData === void 0 ? void 0 : actualData.a11yErrors) || (actualData === null || actualData === void 0 ? void 0 : actualData.violations) || [];
|
|
162
|
+
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
|
+
const filteredSteps = (() => {
|
|
164
|
+
const sRaw = result.steps.map(s => s.title);
|
|
165
|
+
const blocklist = ['Evaluate', 'Create page', 'Close page', 'Before Hooks', 'After Hooks', 'Worker Teardown', 'Worker Cleanup', 'Attach', 'Wait for timeout', 'Capture A11y screenshot'];
|
|
166
|
+
const filtered = result.steps
|
|
167
|
+
.filter((s) => !blocklist.some(b => s.title.includes(b)))
|
|
168
|
+
.map((s) => s.title);
|
|
169
|
+
console.log(`[SnapAlly] Steps for ${test.title}: Raw=${sRaw.length}, Filtered=${filtered.length}`);
|
|
170
|
+
return filtered;
|
|
171
|
+
})();
|
|
172
|
+
const testResults = {
|
|
148
173
|
num: index,
|
|
149
174
|
folderName: testFolderName,
|
|
150
|
-
executionReportPath: `${testFolderName}/${executionReportName}`,
|
|
151
175
|
title: test.title,
|
|
152
|
-
fileName:
|
|
176
|
+
fileName: path.relative(this.projectRoot, test.location.file),
|
|
177
|
+
duration: TimeUtils_1.TimeUtils.formatDuration(result.duration),
|
|
153
178
|
timeDuration: result.duration,
|
|
154
|
-
|
|
155
|
-
description: testMeta.description,
|
|
179
|
+
description: '', // Could extract from annotations if needed
|
|
156
180
|
status: result.status,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
181
|
+
statusIcon: this.getStatusIcon(result.status),
|
|
182
|
+
browser: (() => {
|
|
183
|
+
var _a, _b;
|
|
184
|
+
if (test.outcome() === 'skipped')
|
|
185
|
+
return 'n/a';
|
|
186
|
+
const bName = test._projectId ||
|
|
187
|
+
((_b = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project()) === null || _b === void 0 ? void 0 : _b.name) ||
|
|
188
|
+
test.projectName ||
|
|
189
|
+
'chromium';
|
|
190
|
+
return bName;
|
|
191
|
+
})(),
|
|
192
|
+
adoOrganization: ((_a = this.options.ado) === null || _a === void 0 ? void 0 : _a.organization) || (actualData === null || actualData === void 0 ? void 0 : actualData.adoOrganization),
|
|
193
|
+
adoProject: ((_b = this.options.ado) === null || _b === void 0 ? void 0 : _b.project) || (actualData === null || actualData === void 0 ? void 0 : actualData.adoProject),
|
|
194
|
+
adoAreaPath: ((_c = this.options.ado) === null || _c === void 0 ? void 0 : _c.areaPath) || (actualData === null || actualData === void 0 ? void 0 : actualData.adoAreaPath),
|
|
195
|
+
timestamp: new Date().toLocaleString(),
|
|
196
|
+
pageUrl: (actualData === null || actualData === void 0 ? void 0 : actualData.pageUrl) || (actualData === null || actualData === void 0 ? void 0 : actualData.pageKey) || 'Resource',
|
|
197
|
+
tags: [], // Extract from test tags if available
|
|
198
|
+
preConditions: [],
|
|
199
|
+
steps: filteredSteps,
|
|
200
|
+
postConditions: [],
|
|
201
|
+
videoPath: videoPath.length > 0 ? videoPath : null,
|
|
202
|
+
screenshotPaths,
|
|
203
|
+
attachments,
|
|
204
|
+
errors: result.errors.map((e) => this.renderer.ansiToHtml(e.message || '')),
|
|
205
|
+
a11yErrors: violations.map((v) => ({
|
|
206
|
+
...v,
|
|
207
|
+
target: (v.target || []).map((t) => ({
|
|
208
|
+
...t,
|
|
209
|
+
steps: (t.steps && t.steps.length > 0) ? t.steps : filteredSteps
|
|
210
|
+
}))
|
|
211
|
+
})),
|
|
212
|
+
a11yErrorCount: a11yErrorCount,
|
|
213
|
+
colors: this.colors,
|
|
172
214
|
};
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
extractTestMetadata(test, result) {
|
|
183
|
-
const tags = test.tags.map((t) => t.replace('@', ''));
|
|
184
|
-
const statusIcon = models_1.TestStatusIcon[result.status] || 'help';
|
|
185
|
-
const descAnnotation = test.annotations.find((a) => a.type === 'Description');
|
|
186
|
-
const description = (descAnnotation === null || descAnnotation === void 0 ? void 0 : descAnnotation.description) || 'No Description';
|
|
187
|
-
const steps = result.steps
|
|
188
|
-
.filter((s) => s.category === 'test.step')
|
|
189
|
-
.map((s) => s.title);
|
|
190
|
-
const preConditions = test.annotations
|
|
191
|
-
.filter((a) => a.type === 'Pre Condition')
|
|
192
|
-
.map((a) => a.description || '');
|
|
193
|
-
const postConditions = test.annotations
|
|
194
|
-
.filter((a) => a.type === 'Post Condition')
|
|
195
|
-
.map((a) => a.description || '');
|
|
196
|
-
return { tags, statusIcon, description, steps, preConditions, postConditions };
|
|
197
|
-
}
|
|
198
|
-
/** Determines the browser name for the current test. */
|
|
199
|
-
resolveBrowser(test) {
|
|
200
|
-
const project = test.parent.project();
|
|
201
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
202
|
-
const projectUse = (project === null || project === void 0 ? void 0 : project.use) || {};
|
|
203
|
-
return ((project === null || project === void 0 ? void 0 : project.name) ||
|
|
204
|
-
projectUse.browserName ||
|
|
205
|
-
projectUse.defaultBrowserType ||
|
|
206
|
-
'chromium');
|
|
207
|
-
}
|
|
208
|
-
/** Converts Playwright error objects into HTML-safe strings. */
|
|
209
|
-
extractErrorLogs(result) {
|
|
210
|
-
return (result.errors || []).map((err) => {
|
|
211
|
-
const fullMsg = err.stack
|
|
212
|
-
? `${err.message}\n${err.stack}`
|
|
213
|
-
: err.message || 'Error occurred';
|
|
214
|
-
return this.renderer.ansiToHtml(fullMsg);
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
218
|
-
// Accessibility data processing
|
|
219
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
220
|
-
/** Return value for {@link processAccessibilityData}. */
|
|
221
|
-
async processAccessibilityData(test, result, sanitizedTitle, testResultsFolder, steps, video, browser, errorLogs) {
|
|
222
|
-
var _a;
|
|
223
|
-
const sources = this.collectA11yDataSources(test, result);
|
|
224
|
-
if (sources.length === 0) {
|
|
225
|
-
return { errorCount: 0, errors: [] };
|
|
226
|
-
}
|
|
227
|
-
let reportPath;
|
|
228
|
-
let errorCount = 0;
|
|
229
|
-
const aggregatedErrors = [];
|
|
230
|
-
let pageUrl;
|
|
231
|
-
for (const [index, source] of sources.entries()) {
|
|
232
|
-
const reportData = this.parseA11ySource(source, errorLogs);
|
|
233
|
-
if (!reportData)
|
|
234
|
-
continue;
|
|
235
|
-
const reportName = this.buildA11yReportName(sanitizedTitle, reportData.pageKey, index, sources.length);
|
|
236
|
-
reportPath = reportName;
|
|
237
|
-
this.applyReportConfig(reportData, video);
|
|
238
|
-
this.backfillSteps(reportData, steps);
|
|
239
|
-
const auditFile = path.join(testResultsFolder, reportName);
|
|
240
|
-
await this.renderer.render('accessibility-report.html', { data: reportData, folderTest: testResultsFolder }, testResultsFolder, auditFile);
|
|
241
|
-
if (reportData.pageUrl && !pageUrl) {
|
|
242
|
-
pageUrl = reportData.pageUrl;
|
|
243
|
-
}
|
|
244
|
-
// Aggregate a11y errors into browser & global summaries
|
|
245
|
-
if ((_a = reportData.a11yErrors) === null || _a === void 0 ? void 0 : _a.length) {
|
|
246
|
-
const scanCount = this.aggregateA11yErrors(reportData.a11yErrors, browser);
|
|
247
|
-
errorCount += scanCount;
|
|
248
|
-
aggregatedErrors.push(...reportData.a11yErrors);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return { reportPath, errorCount, errors: aggregatedErrors, pageUrl };
|
|
252
|
-
}
|
|
253
|
-
/** Collects all A11y data sources (attachments + annotations) for a test. */
|
|
254
|
-
collectA11yDataSources(test, result) {
|
|
255
|
-
const attachments = (result.attachments || [])
|
|
256
|
-
.filter((a) => a.name === 'A11y')
|
|
257
|
-
.map((a) => ({ type: 'attachment', data: a }));
|
|
258
|
-
const annotations = (test.annotations || [])
|
|
259
|
-
.filter((a) => a.type === 'A11y')
|
|
260
|
-
.map((a) => ({ type: 'annotation', data: a }));
|
|
261
|
-
return [...attachments, ...annotations];
|
|
262
|
-
}
|
|
263
|
-
/** Attempts to parse a single A11y data source into a ReportData object. */
|
|
264
|
-
parseA11ySource(source, errorLogs) {
|
|
265
|
-
try {
|
|
266
|
-
if (source.type === 'attachment') {
|
|
267
|
-
const attach = source.data;
|
|
268
|
-
if (attach.body) {
|
|
269
|
-
return JSON.parse(attach.body.toString());
|
|
270
|
-
}
|
|
271
|
-
if (attach.path && fs.existsSync(attach.path)) {
|
|
272
|
-
return JSON.parse(fs.readFileSync(attach.path, 'utf-8'));
|
|
273
|
-
}
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
// annotation
|
|
277
|
-
const annot = source.data;
|
|
278
|
-
return JSON.parse(annot.description || '{}');
|
|
279
|
-
}
|
|
280
|
-
catch (e) {
|
|
281
|
-
console.error(`[SnapAlly] Failed to parse A11y ${source.type}: ${e}`);
|
|
282
|
-
errorLogs.push(this.renderer.ansiToHtml(`[SnapAlly] Internal error parsing accessibility data from ${source.type}: ${e}`));
|
|
283
|
-
return null;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
/** Generates a sanitized HTML filename for an accessibility report. */
|
|
287
|
-
buildA11yReportName(sanitizedTitle, pageKey, index, totalScans) {
|
|
288
|
-
const hasMultiple = totalScans > 1;
|
|
289
|
-
const suffix = hasMultiple ? `-${index + 1}` : '';
|
|
290
|
-
if (pageKey) {
|
|
291
|
-
const sanitizedKey = pageKey
|
|
292
|
-
.replace(/https?:\/\//, '')
|
|
293
|
-
.replace(/[^a-z0-9]+/gi, '-')
|
|
294
|
-
.replace(/^-+|-+$/g, '')
|
|
295
|
-
.toLowerCase();
|
|
296
|
-
if (sanitizedKey) {
|
|
297
|
-
return `${sanitizedKey}${suffix}.html`;
|
|
298
|
-
}
|
|
215
|
+
const reportFileName = 'report.html';
|
|
216
|
+
const a11yReportFileName = 'accessibility-report.html';
|
|
217
|
+
testResults.executionReportPath = `${testFolderName}/${reportFileName}`;
|
|
218
|
+
// Generate separate accessibility report if there are a11y errors
|
|
219
|
+
if (violations.length > 0) {
|
|
220
|
+
testResults.a11yReportPath = `${testFolderName}/${a11yReportFileName}`;
|
|
221
|
+
const a11yReportPath = path.join(testFolder, a11yReportFileName);
|
|
222
|
+
console.log(`[SnapAlly] Generating A11y report for ${test.title} (Browser: ${testResults.browser})`);
|
|
223
|
+
await this.renderer.render('accessibility-report.html', testResults, testFolder, a11yReportPath);
|
|
299
224
|
}
|
|
300
|
-
|
|
225
|
+
console.log(`[SnapAlly] Data state for "${test.title}": browser=${testResults.browser}, a11yErrors=${((_d = testResults.a11yErrors) === null || _d === void 0 ? void 0 : _d.length) || 0}`);
|
|
226
|
+
const reportPath = path.join(testFolder, reportFileName);
|
|
227
|
+
await this.renderer.render('test-execution-report.html', testResults, testFolder, reportPath);
|
|
228
|
+
this.updateSummary(test, testResults);
|
|
301
229
|
}
|
|
302
|
-
|
|
303
|
-
applyReportConfig(reportData, video) {
|
|
304
|
-
reportData.criticalColor = this.colors.critical;
|
|
305
|
-
reportData.seriousColor = this.colors.serious;
|
|
306
|
-
reportData.moderateColor = this.colors.moderate;
|
|
307
|
-
reportData.minorColor = this.colors.minor;
|
|
308
|
-
if (this.options.ado) {
|
|
309
|
-
reportData.adoOrganization =
|
|
310
|
-
this.options.ado.organization || reportData.adoOrganization;
|
|
311
|
-
reportData.adoProject = this.options.ado.project || reportData.adoProject;
|
|
312
|
-
if (this.options.ado.areaPath) {
|
|
313
|
-
reportData.adoAreaPath = this.options.ado.areaPath;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
if (video) {
|
|
317
|
-
reportData.video = video;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Backfills reproduction steps from `test.step` calls into a11y targets
|
|
322
|
-
* that have no steps recorded (e.g. violations found via static scan).
|
|
323
|
-
*/
|
|
324
|
-
backfillSteps(reportData, steps) {
|
|
325
|
-
const filteredSteps = steps.filter((s) => !s.includes('Capture A11y screenshot'));
|
|
326
|
-
if (filteredSteps.length === 0)
|
|
327
|
-
return;
|
|
328
|
-
for (const err of reportData.a11yErrors) {
|
|
329
|
-
for (const target of err.target) {
|
|
330
|
-
if (!target.steps || target.steps.length === 0) {
|
|
331
|
-
target.steps = filteredSteps;
|
|
332
|
-
target.stepsJson = JSON.stringify(filteredSteps);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
// ──────────────────────────────���─────────────────────────────────────────
|
|
338
|
-
// Summary aggregation
|
|
339
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
340
|
-
/**
|
|
341
|
-
* Aggregates a11y error counts into both the browser-specific and global
|
|
342
|
-
* summaries. Returns the total error count for this scan.
|
|
343
|
-
*/
|
|
344
|
-
aggregateA11yErrors(errors, browser) {
|
|
345
|
-
const bSummary = this.getOrCreateBrowserSummary(browser);
|
|
346
|
-
let scanErrorCount = 0;
|
|
347
|
-
for (const err of errors) {
|
|
348
|
-
const count = err.total || 0;
|
|
349
|
-
scanErrorCount += count;
|
|
350
|
-
// Browser-level aggregation
|
|
351
|
-
if (!bSummary.wcagErrors[err.id]) {
|
|
352
|
-
bSummary.wcagErrors[err.id] = {
|
|
353
|
-
count: 0,
|
|
354
|
-
severity: err.severity,
|
|
355
|
-
helpUrl: err.helpUrl,
|
|
356
|
-
description: err.description,
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
bSummary.wcagErrors[err.id].count += count;
|
|
360
|
-
// Global aggregation
|
|
361
|
-
if (!this.executionSummary.wcagErrors[err.id]) {
|
|
362
|
-
this.executionSummary.wcagErrors[err.id] = {
|
|
363
|
-
count: 0,
|
|
364
|
-
severity: err.severity,
|
|
365
|
-
helpUrl: err.helpUrl,
|
|
366
|
-
description: err.description,
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
this.executionSummary.wcagErrors[err.id].count += count;
|
|
370
|
-
}
|
|
371
|
-
bSummary.totalA11yErrorCount += scanErrorCount;
|
|
372
|
-
this.executionSummary.totalA11yErrorCount += scanErrorCount;
|
|
373
|
-
return scanErrorCount;
|
|
374
|
-
}
|
|
375
|
-
/** Updates the browser-specific test counts (passed/failed/skipped). */
|
|
376
|
-
updateBrowserSummary(browser, status) {
|
|
377
|
-
const bSummary = this.getOrCreateBrowserSummary(browser);
|
|
378
|
-
bSummary.total++;
|
|
230
|
+
getStatusIcon(status) {
|
|
379
231
|
switch (status) {
|
|
380
232
|
case 'passed':
|
|
381
|
-
|
|
382
|
-
break;
|
|
233
|
+
return models_1.TestStatusIcon.passed;
|
|
383
234
|
case 'failed':
|
|
384
|
-
|
|
385
|
-
|
|
235
|
+
case 'timedOut':
|
|
236
|
+
return models_1.TestStatusIcon.failed;
|
|
386
237
|
case 'skipped':
|
|
387
|
-
|
|
388
|
-
|
|
238
|
+
return models_1.TestStatusIcon.skipped;
|
|
239
|
+
default:
|
|
240
|
+
return 'help';
|
|
389
241
|
}
|
|
390
242
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
if (
|
|
395
|
-
this.executionSummary.
|
|
396
|
-
switch (result.status) {
|
|
397
|
-
case 'passed':
|
|
398
|
-
this.executionSummary.totalPassed++;
|
|
399
|
-
break;
|
|
400
|
-
case 'failed':
|
|
401
|
-
this.executionSummary.totalFailed++;
|
|
402
|
-
break;
|
|
403
|
-
case 'skipped':
|
|
404
|
-
this.executionSummary.totalSkipped++;
|
|
405
|
-
break;
|
|
243
|
+
updateSummary(test, result) {
|
|
244
|
+
var _a, _b;
|
|
245
|
+
const browser = result.browser;
|
|
246
|
+
if (!this.executionSummary.groupedResults[browser]) {
|
|
247
|
+
this.executionSummary.groupedResults[browser] = [];
|
|
406
248
|
}
|
|
407
|
-
this.executionSummary.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
412
|
-
/** Ensures a file group key exists in the grouped results map. */
|
|
413
|
-
ensureGroupExists(fileGroup) {
|
|
414
|
-
if (!this.executionSummary.groupedResults[fileGroup]) {
|
|
415
|
-
this.executionSummary.groupedResults[fileGroup] = [];
|
|
249
|
+
this.executionSummary.groupedResults[browser].push(result);
|
|
250
|
+
// Initialize browser summary if needed
|
|
251
|
+
if (!this.executionSummary.browserSummaries) {
|
|
252
|
+
this.executionSummary.browserSummaries = {};
|
|
416
253
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const summaries = this.executionSummary.browserSummaries;
|
|
421
|
-
if (!summaries[browser]) {
|
|
422
|
-
summaries[browser] = {
|
|
423
|
-
duration: '0s',
|
|
254
|
+
if (!this.executionSummary.browserSummaries[browser]) {
|
|
255
|
+
this.executionSummary.browserSummaries[browser] = {
|
|
256
|
+
duration: '',
|
|
424
257
|
status: '',
|
|
425
258
|
statusIcon: '',
|
|
426
259
|
total: 0,
|
|
@@ -433,7 +266,63 @@ class SnapAllyReporter {
|
|
|
433
266
|
totalA11yErrorCount: 0,
|
|
434
267
|
};
|
|
435
268
|
}
|
|
436
|
-
|
|
269
|
+
const bSummary = this.executionSummary.browserSummaries[browser];
|
|
270
|
+
this.executionSummary.total++;
|
|
271
|
+
bSummary.total++;
|
|
272
|
+
if (result.status === 'passed') {
|
|
273
|
+
this.executionSummary.totalPassed++;
|
|
274
|
+
bSummary.totalPassed++;
|
|
275
|
+
}
|
|
276
|
+
else if (result.status === 'failed' || result.status === 'timedOut') {
|
|
277
|
+
this.executionSummary.totalFailed++;
|
|
278
|
+
bSummary.totalFailed++;
|
|
279
|
+
}
|
|
280
|
+
else if (result.status === 'skipped') {
|
|
281
|
+
this.executionSummary.totalSkipped++;
|
|
282
|
+
bSummary.totalSkipped++;
|
|
283
|
+
}
|
|
284
|
+
const testKey = test.titlePath().join(' > ');
|
|
285
|
+
const violations = result.a11yErrors || result.violations;
|
|
286
|
+
if (violations && violations.length > 0) {
|
|
287
|
+
const count = result.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);
|
|
288
|
+
// De-duplicate global count across browsers for same test case
|
|
289
|
+
const prevTestGlobalCount = this.testGlobalCounts[testKey] || 0;
|
|
290
|
+
if (count > prevTestGlobalCount) {
|
|
291
|
+
this.executionSummary.totalA11yErrorCount += (count - prevTestGlobalCount);
|
|
292
|
+
this.testGlobalCounts[testKey] = count;
|
|
293
|
+
}
|
|
294
|
+
bSummary.totalA11yErrorCount += count;
|
|
295
|
+
for (const err of violations) {
|
|
296
|
+
const ruleId = err.id;
|
|
297
|
+
const occCount = (err.total || ((_a = err.target) === null || _a === void 0 ? void 0 : _a.length) || ((_b = err.nodes) === null || _b === void 0 ? void 0 : _b.length) || 0);
|
|
298
|
+
// Update global wcagErrors (de-duplicated)
|
|
299
|
+
if (!this.executionSummary.wcagErrors[ruleId]) {
|
|
300
|
+
this.executionSummary.wcagErrors[ruleId] = {
|
|
301
|
+
count: 0,
|
|
302
|
+
severity: err.severity || err.impact,
|
|
303
|
+
helpUrl: err.helpUrl,
|
|
304
|
+
description: err.description,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (!this.testRuleCounts[testKey])
|
|
308
|
+
this.testRuleCounts[testKey] = {};
|
|
309
|
+
const prevRuleOccCount = this.testRuleCounts[testKey][ruleId] || 0;
|
|
310
|
+
if (occCount > prevRuleOccCount) {
|
|
311
|
+
this.executionSummary.wcagErrors[ruleId].count += (occCount - prevRuleOccCount);
|
|
312
|
+
this.testRuleCounts[testKey][ruleId] = occCount;
|
|
313
|
+
}
|
|
314
|
+
// Update browser-specific wcagErrors (per browser, usually naturally unique)
|
|
315
|
+
if (!bSummary.wcagErrors[ruleId]) {
|
|
316
|
+
bSummary.wcagErrors[ruleId] = {
|
|
317
|
+
count: 0,
|
|
318
|
+
severity: err.severity || err.impact,
|
|
319
|
+
helpUrl: err.helpUrl,
|
|
320
|
+
description: err.description,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
bSummary.wcagErrors[ruleId].count += occCount;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
437
326
|
}
|
|
438
327
|
}
|
|
439
328
|
exports.default = SnapAllyReporter;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles the rendering of HTML reports using static templates and JSON data injection.
|
|
3
|
+
*/
|
|
4
|
+
export declare class A11yHtmlRenderer {
|
|
5
|
+
/**
|
|
6
|
+
* Renders a static HTML template by copying it and generating the accompanied data payload.
|
|
7
|
+
* @param templateName The template file name in the templates folder.
|
|
8
|
+
* @param data The data object to pass to the client-side JS app.
|
|
9
|
+
* @param outputFolder The folder where the rendered file will be saved.
|
|
10
|
+
* @param outputFileName The full path of the output file.
|
|
11
|
+
*/
|
|
12
|
+
render(templateName: string, data: Record<string, unknown>, outputFolder: string, outputFileName: string): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Converts ANSI color codes to HTML spans for nicer error display.
|
|
15
|
+
*/
|
|
16
|
+
ansiToHtml(text: string): string;
|
|
17
|
+
}
|