snap-ally 0.1.1-beta → 0.2.2-beta

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.
@@ -39,20 +39,39 @@ const A11yHtmlRenderer_1 = require("./A11yHtmlRenderer");
39
39
  const A11yTimeUtils_1 = require("./A11yTimeUtils");
40
40
  const path = __importStar(require("path"));
41
41
  const fs = __importStar(require("fs"));
42
+ /** Default severity colors used when the user doesn't override them. */
43
+ const DEFAULT_COLORS = {
44
+ critical: '#c92a2a',
45
+ serious: '#e67700',
46
+ moderate: '#ca8a04',
47
+ minor: '#0891b2',
48
+ };
49
+ // ────────────────────────────────────────────────────────────────────────────
50
+ // Reporter
51
+ // ────────────────────────────────────────────────────────────────────────────
42
52
  /**
43
53
  * Playwright reporter for accessibility audits and test steps.
44
- * Generates an execution summary and detailed reports per test.
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
45
59
  */
46
60
  class SnapAllyReporter {
47
- printsToStdio() {
48
- return false;
49
- }
50
61
  constructor(options = {}) {
51
- this.testIndex = 0;
62
+ var _a, _b, _c, _d;
52
63
  this.assetsManager = new A11yReportAssets_1.A11yReportAssets();
53
64
  this.renderer = new A11yHtmlRenderer_1.A11yHtmlRenderer();
54
65
  this.projectRoot = 'tests';
55
- // Global summary tracking
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
+ this.testIndex = 0;
72
+ /** Async tasks queued by `onTestEnd`; drained in `onEnd`. */
73
+ this.tasks = [];
74
+ /** Aggregated data for the final summary report. */
56
75
  this.executionSummary = {
57
76
  duration: '',
58
77
  status: '',
@@ -68,221 +87,293 @@ class SnapAllyReporter {
68
87
  browserSummaries: {},
69
88
  date: '',
70
89
  };
71
- // Track async tasks to ensure they finish before onEnd
72
- this.tasks = [];
73
90
  this.options = options;
74
91
  this.outputFolder = path.resolve(process.cwd(), options.outputFolder || 'steps-report');
92
+ 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,
97
+ };
98
+ }
99
+ printsToStdio() {
100
+ return false;
75
101
  }
76
102
  onBegin(config) {
77
103
  this.projectRoot = config.rootDir || 'tests';
78
104
  }
79
105
  onTestEnd(test, result) {
80
- this.tasks.push(this.processTestResult(test, result));
106
+ // Increment index synchronously so concurrent tasks never share an index.
107
+ const index = ++this.testIndex;
108
+ this.tasks.push(this.processTestResult(test, result, index));
109
+ }
110
+ async onEnd(result) {
111
+ await Promise.all(this.tasks);
112
+ this.executionSummary.duration = A11yTimeUtils_1.A11yTimeUtils.formatDuration(result.duration);
113
+ this.executionSummary.status = result.status;
114
+ this.executionSummary.statusIcon =
115
+ models_1.TestStatusIcon[result.status] || 'help';
116
+ this.executionSummary.date = A11yTimeUtils_1.A11yTimeUtils.formatDate(new Date());
117
+ const summaryFile = path.join(this.outputFolder, 'summary.html');
118
+ await this.renderer.render('execution-summary.html', { ...this.executionSummary, colors: this.colors }, this.outputFolder, summaryFile);
119
+ console.log(`\n[SnapAlly] Reports generated in: ${path.resolve(this.outputFolder)}`);
81
120
  }
82
- async processTestResult(test, result) {
83
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
84
- this.testIndex++;
121
+ // ────────────────────────────────────────────────────────────────────────
122
+ // Core per-test processing
123
+ // ────────────────────────────────────────────────────────────────────────
124
+ async processTestResult(test, result, index) {
85
125
  const sanitizedTitle = test.title.replace(/[^a-z0-9]+/gi, '-').toLowerCase();
86
- const testFolderName = `${this.testIndex}-${sanitizedTitle}`;
126
+ const testFolderName = `${index}-${sanitizedTitle}`;
87
127
  const testResultsFolder = path.join(this.outputFolder, testFolderName);
88
- // --- 1. Functional Step Reporting ---
89
128
  const fileGroup = path.relative(this.projectRoot, test.location.file);
90
- if (!this.executionSummary.groupedResults[fileGroup]) {
91
- this.executionSummary.groupedResults[fileGroup] = [];
92
- }
129
+ this.ensureGroupExists(fileGroup);
130
+ const browser = this.resolveBrowser(test);
131
+ const testMeta = this.extractTestMetadata(test, result);
132
+ // Copy assets
133
+ const video = await this.assetsManager.copyTestVideo(result, testResultsFolder);
134
+ const screenshots = this.assetsManager.copyScreenshots(result, testResultsFolder);
135
+ const allAttachments = [
136
+ ...this.assetsManager.copyPngAttachments(result, testResultsFolder),
137
+ ...this.assetsManager.copyAllOtherAttachments(result, testResultsFolder),
138
+ ];
139
+ const errorLogs = this.extractErrorLogs(result);
140
+ // Accessibility processing
141
+ const a11yResult = await this.processAccessibilityData(test, result, sanitizedTitle, testResultsFolder, testMeta.steps, video, browser, errorLogs);
142
+ // Update browser & global summary counts
143
+ this.updateBrowserSummary(browser, result.status);
144
+ this.updateGlobalSummary(test, result);
145
+ // Build the test stats object
146
+ const executionReportName = `execution-${sanitizedTitle}.html`;
147
+ const testStats = {
148
+ num: index,
149
+ folderName: testFolderName,
150
+ executionReportPath: `${testFolderName}/${executionReportName}`,
151
+ title: test.title,
152
+ fileName: fileGroup,
153
+ timeDuration: result.duration,
154
+ duration: A11yTimeUtils_1.A11yTimeUtils.formatDuration(result.duration),
155
+ description: testMeta.description,
156
+ status: result.status,
157
+ browser,
158
+ tags: testMeta.tags,
159
+ preConditions: testMeta.preConditions,
160
+ steps: testMeta.steps,
161
+ postConditions: testMeta.postConditions,
162
+ statusIcon: testMeta.statusIcon,
163
+ pageUrl: a11yResult.pageUrl,
164
+ videoPath: video,
165
+ screenshotPaths: screenshots,
166
+ attachments: allAttachments,
167
+ errors: errorLogs,
168
+ a11yReportPath: a11yResult.reportPath,
169
+ a11yErrorCount: a11yResult.errorCount,
170
+ a11yErrors: a11yResult.errors,
171
+ colors: this.options.colors,
172
+ };
173
+ this.executionSummary.groupedResults[fileGroup].push(testStats);
174
+ // Render the per-test execution report
175
+ const indexFile = path.join(testResultsFolder, executionReportName);
176
+ await this.renderer.render('test-execution-report.html', { ...testStats, colors: this.colors }, testResultsFolder, indexFile);
177
+ }
178
+ // ────────────────────────────────────────────────────────────────────────
179
+ // Metadata extraction
180
+ // ────────────────────────────────────────────────────────────────────────
181
+ /** Extracts structured metadata from test annotations and result steps. */
182
+ extractTestMetadata(test, result) {
93
183
  const tags = test.tags.map((t) => t.replace('@', ''));
94
184
  const statusIcon = models_1.TestStatusIcon[result.status] || 'help';
95
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
- const projectUse = ((_a = test.parent.project()) === null || _a === void 0 ? void 0 : _a.use) || {};
97
- const browser = ((_b = test.parent.project()) === null || _b === void 0 ? void 0 : _b.name) ||
98
- projectUse.browserName ||
99
- projectUse.defaultBrowserType ||
100
- 'chromium';
101
185
  const descAnnotation = test.annotations.find((a) => a.type === 'Description');
102
186
  const description = (descAnnotation === null || descAnnotation === void 0 ? void 0 : descAnnotation.description) || 'No Description';
103
- // Prepare steps from actual test.step calls instead of just annotations
104
- const steps = result.steps.filter((s) => s.category === 'test.step').map((s) => s.title);
187
+ const steps = result.steps
188
+ .filter((s) => s.category === 'test.step')
189
+ .map((s) => s.title);
105
190
  const preConditions = test.annotations
106
191
  .filter((a) => a.type === 'Pre Condition')
107
192
  .map((a) => a.description || '');
108
193
  const postConditions = test.annotations
109
194
  .filter((a) => a.type === 'Post Condition')
110
195
  .map((a) => a.description || '');
111
- const video = await this.assetsManager.copyTestVideo(result, testResultsFolder);
112
- const screenshots = this.assetsManager.copyScreenshots(result, testResultsFolder);
113
- const pngAttachments = this.assetsManager.copyPngAttachments(result, testResultsFolder);
114
- const otherAttachments = this.assetsManager.copyAllOtherAttachments(result, testResultsFolder);
115
- const allAttachments = [...pngAttachments, ...otherAttachments];
116
- console.log(`[SnapAlly Debug] Test "${test.title}" ended. Status: ${result.status}. Video: ${video ? 'Created' : 'Missing'}`);
117
- console.log(`[SnapAlly Debug] Raw Attachments: ${result.attachments.map((a) => `${a.name} (${a.path ? 'file' : 'body'})`).join(', ')}`);
118
- const errorLogs = (result.errors || []).map((err) => {
196
+ return { tags, statusIcon, description, steps, preConditions, postConditions };
197
+ }
198
+ /** Determines the browser name for the current test. */
199
+ resolveBrowser(test) {
200
+ var _a, _b;
201
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
202
+ const projectUse = ((_a = test.parent.project()) === null || _a === void 0 ? void 0 : _a.use) || {};
203
+ return (((_b = test.parent.project()) === null || _b === void 0 ? void 0 : _b.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) => {
119
211
  const fullMsg = err.stack
120
212
  ? `${err.message}\n${err.stack}`
121
213
  : err.message || 'Error occurred';
122
214
  return this.renderer.ansiToHtml(fullMsg);
123
- }) || [];
124
- // --- 2. Accessibility Reporting (Iterate over all A11y sources: attachments and annotations) ---
125
- const a11yAttachments = (result.attachments || []).filter((a) => a.name === 'A11y');
126
- const a11yAnnotations = (test.annotations || []).filter((a) => a.type === 'A11y');
127
- const a11yDataSources = [
128
- ...a11yAttachments.map((a) => ({ type: 'attachment', data: a })),
129
- ...a11yAnnotations.map((a) => ({ type: 'annotation', data: a })),
130
- ];
131
- if (a11yDataSources.length === 0) {
132
- console.error(`[SnapAlly Debug] No A11y data sources found for test: ${test.title}`);
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: [] };
133
226
  }
134
- let a11yReportPath = undefined;
135
- let a11yErrorCount = 0;
136
- let aggregatedA11yErrors = [];
137
- let testStatsPageUrl = undefined;
138
- // Loop through all accessibility scans in this test
139
- for (const [index, source] of a11yDataSources.entries()) {
140
- let reportData;
141
- try {
142
- if (source.type === 'attachment') {
143
- const attach = source.data;
144
- if (attach.body) {
145
- reportData = JSON.parse(attach.body.toString());
146
- }
147
- else if (attach.path && fs.existsSync(attach.path)) {
148
- reportData = JSON.parse(fs.readFileSync(attach.path, 'utf-8'));
149
- }
150
- else {
151
- continue;
152
- }
153
- }
154
- else {
155
- const annot = source.data;
156
- reportData = JSON.parse(annot.description || '{}');
157
- }
158
- }
159
- catch (e) {
160
- console.error(`[SnapAlly] Failed to parse A11y ${source.type}: ${e}`);
161
- errorLogs.push(this.renderer.ansiToHtml(`[SnapAlly] Internal error parsing accessibility data from ${source.type}: ${e}`));
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)
162
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;
163
243
  }
164
- // Determine Report Name (append index if multiple)
165
- let a11yReportName = `accessibility-${sanitizedTitle}.html`;
166
- if (a11yDataSources.length > 1) {
167
- a11yReportName = `accessibility-${sanitizedTitle}-${index + 1}.html`;
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);
168
249
  }
169
- // Sanitize pageKey for filename override if present
170
- if (reportData.pageKey) {
171
- const sanitizedKey = reportData.pageKey
172
- .replace(/https?:\/\//, '')
173
- .replace(/[^a-z0-9]+/gi, '-')
174
- .replace(/^-+|-+$/g, '')
175
- .toLowerCase();
176
- if (sanitizedKey) {
177
- a11yReportName =
178
- a11yDataSources.length > 1
179
- ? `${sanitizedKey}-${index + 1}.html`
180
- : `${sanitizedKey}.html`;
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());
181
270
  }
271
+ if (attach.path && fs.existsSync(attach.path)) {
272
+ return JSON.parse(fs.readFileSync(attach.path, 'utf-8'));
273
+ }
274
+ return null;
182
275
  }
183
- // Set the main report path to the LAST one (or maybe first? using last for now)
184
- a11yReportPath = a11yReportName;
185
- // Re-apply configuration
186
- reportData.criticalColor = ((_c = this.options.colors) === null || _c === void 0 ? void 0 : _c.critical) || '#c92a2a';
187
- reportData.seriousColor = ((_d = this.options.colors) === null || _d === void 0 ? void 0 : _d.serious) || '#e67700';
188
- reportData.moderateColor = ((_e = this.options.colors) === null || _e === void 0 ? void 0 : _e.moderate) || '#ca8a04';
189
- reportData.minorColor = ((_f = this.options.colors) === null || _f === void 0 ? void 0 : _f.minor) || '#0891b2';
190
- if (this.options.ado) {
191
- reportData.adoOrganization =
192
- this.options.ado.organization || reportData.adoOrganization;
193
- reportData.adoProject = this.options.ado.project || reportData.adoProject;
194
- }
195
- // Sync video name
196
- if (video)
197
- reportData.video = video;
198
- // Backfill steps from actual test.step calls if annotations were empty
199
- const filteredSteps = steps.filter((s) => !s.includes('Capture A11y screenshot'));
200
- if (filteredSteps.length > 0) {
201
- reportData.a11yErrors.forEach((err) => {
202
- err.target.forEach((t) => {
203
- if (!t.steps || t.steps.length === 0) {
204
- t.steps = filteredSteps;
205
- t.stepsJson = JSON.stringify(filteredSteps);
206
- }
207
- });
208
- });
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`;
209
298
  }
210
- const auditFile = path.join(testResultsFolder, a11yReportName);
211
- await this.renderer.render('accessibility-report.html', { data: reportData, folderTest: testResultsFolder }, testResultsFolder, auditFile);
212
- // Capture the first page URL for the testStats if not already set
213
- if (reportData.pageUrl && !testStatsPageUrl) {
214
- testStatsPageUrl = reportData.pageUrl;
299
+ }
300
+ return `accessibility-${sanitizedTitle}${suffix}.html`;
301
+ }
302
+ /** Applies reporter-level configuration (colors, ADO, video) to a ReportData object. */
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
+ }
313
+ if (video) {
314
+ reportData.video = video;
315
+ }
316
+ }
317
+ /**
318
+ * Backfills reproduction steps from `test.step` calls into a11y targets
319
+ * that have no steps recorded (e.g. violations found via static scan).
320
+ */
321
+ backfillSteps(reportData, steps) {
322
+ const filteredSteps = steps.filter((s) => !s.includes('Capture A11y screenshot'));
323
+ if (filteredSteps.length === 0)
324
+ return;
325
+ for (const err of reportData.a11yErrors) {
326
+ for (const target of err.target) {
327
+ if (!target.steps || target.steps.length === 0) {
328
+ target.steps = filteredSteps;
329
+ target.stepsJson = JSON.stringify(filteredSteps);
330
+ }
215
331
  }
216
- // --- 3. Update Browser-Specific Summary (Partial Aggregation) ---
217
- if (!this.executionSummary.browserSummaries[browser]) {
218
- this.executionSummary.browserSummaries[browser] = {
219
- duration: '0s',
220
- status: '',
221
- statusIcon: '',
222
- total: 0,
223
- totalFailed: 0,
224
- totalFlaky: 0,
225
- totalPassed: 0,
226
- totalSkipped: 0,
227
- groupedResults: {},
228
- wcagErrors: {},
229
- totalA11yErrorCount: 0,
332
+ }
333
+ }
334
+ // ──────────────────────────────���─────────────────────────────────────────
335
+ // Summary aggregation
336
+ // ────────────────────────────────────────────────────────────────────────
337
+ /**
338
+ * Aggregates a11y error counts into both the browser-specific and global
339
+ * summaries. Returns the total error count for this scan.
340
+ */
341
+ aggregateA11yErrors(errors, browser) {
342
+ const bSummary = this.getOrCreateBrowserSummary(browser);
343
+ let scanErrorCount = 0;
344
+ for (const err of errors) {
345
+ const count = err.total || 0;
346
+ scanErrorCount += count;
347
+ // Browser-level aggregation
348
+ if (!bSummary.wcagErrors[err.id]) {
349
+ bSummary.wcagErrors[err.id] = {
350
+ count: 0,
351
+ severity: err.severity,
352
+ helpUrl: err.helpUrl,
353
+ description: err.description,
230
354
  };
231
355
  }
232
- const bSummary = this.executionSummary.browserSummaries[browser];
233
- if (reportData.a11yErrors && reportData.a11yErrors.length > 0) {
234
- // Aggregate counts
235
- const scanErrorCount = reportData.a11yErrors.reduce((sum, err) => sum + (err.total || 0), 0);
236
- a11yErrorCount += scanErrorCount;
237
- aggregatedA11yErrors.push(...reportData.a11yErrors);
238
- reportData.a11yErrors.forEach((err) => {
239
- const rule = err.id;
240
- // Local Browser aggregation
241
- if (!bSummary.wcagErrors[rule]) {
242
- bSummary.wcagErrors[rule] = {
243
- count: 0,
244
- severity: err.severity,
245
- helpUrl: err.helpUrl,
246
- description: err.description,
247
- };
248
- }
249
- bSummary.wcagErrors[rule].count += err.total || 0;
250
- // Global aggregation (always add to ensure summary is not empty)
251
- if (!this.executionSummary.wcagErrors[rule]) {
252
- this.executionSummary.wcagErrors[rule] = {
253
- count: 0,
254
- severity: err.severity,
255
- helpUrl: err.helpUrl,
256
- description: err.description,
257
- };
258
- }
259
- this.executionSummary.wcagErrors[rule].count += err.total || 0;
260
- });
261
- // Update total error counts
262
- bSummary.totalA11yErrorCount += scanErrorCount;
263
- this.executionSummary.totalA11yErrorCount += scanErrorCount;
356
+ bSummary.wcagErrors[err.id].count += count;
357
+ // Global aggregation
358
+ if (!this.executionSummary.wcagErrors[err.id]) {
359
+ this.executionSummary.wcagErrors[err.id] = {
360
+ count: 0,
361
+ severity: err.severity,
362
+ helpUrl: err.helpUrl,
363
+ description: err.description,
364
+ };
264
365
  }
366
+ this.executionSummary.wcagErrors[err.id].count += count;
265
367
  }
266
- // --- 4. Final Aggregation and Test Stats ---
267
- // Update browser summary counts (always, even if no a11y scan occurred)
268
- if (!this.executionSummary.browserSummaries[browser]) {
269
- this.executionSummary.browserSummaries[browser] = {
270
- duration: '0s',
271
- status: '',
272
- statusIcon: '',
273
- total: 0,
274
- totalFailed: 0,
275
- totalFlaky: 0,
276
- totalPassed: 0,
277
- totalSkipped: 0,
278
- groupedResults: {},
279
- wcagErrors: {},
280
- totalA11yErrorCount: 0,
281
- };
282
- }
283
- const bSummary = this.executionSummary.browserSummaries[browser];
368
+ bSummary.totalA11yErrorCount += scanErrorCount;
369
+ this.executionSummary.totalA11yErrorCount += scanErrorCount;
370
+ return scanErrorCount;
371
+ }
372
+ /** Updates the browser-specific test counts (passed/failed/skipped). */
373
+ updateBrowserSummary(browser, status) {
374
+ const bSummary = this.getOrCreateBrowserSummary(browser);
284
375
  bSummary.total++;
285
- switch (result.status) {
376
+ switch (status) {
286
377
  case 'passed':
287
378
  bSummary.totalPassed++;
288
379
  break;
@@ -293,35 +384,9 @@ class SnapAllyReporter {
293
384
  bSummary.totalSkipped++;
294
385
  break;
295
386
  }
296
- const executionReportName = `execution-${sanitizedTitle}.html`;
297
- const testStats = {
298
- num: this.testIndex,
299
- folderName: testFolderName,
300
- executionReportPath: `${testFolderName}/${executionReportName}`,
301
- title: test.title,
302
- fileName: fileGroup,
303
- timeDuration: result.duration,
304
- duration: A11yTimeUtils_1.A11yTimeUtils.formatDuration(result.duration),
305
- description,
306
- status: result.status,
307
- browser,
308
- tags,
309
- preConditions,
310
- steps,
311
- postConditions,
312
- statusIcon,
313
- pageUrl: testStatsPageUrl,
314
- videoPath: video,
315
- screenshotPaths: screenshots,
316
- attachments: allAttachments,
317
- errors: errorLogs,
318
- a11yReportPath,
319
- a11yErrorCount,
320
- a11yErrors: aggregatedA11yErrors,
321
- colors: this.options.colors,
322
- };
323
- this.executionSummary.groupedResults[fileGroup].push(testStats);
324
- // Update summary counts
387
+ }
388
+ /** Updates the global execution summary counts. */
389
+ updateGlobalSummary(test, result) {
325
390
  const isFlaky = test.results.length > 1 && result.status === 'passed';
326
391
  if (isFlaky)
327
392
  this.executionSummary.totalFlaky++;
@@ -337,35 +402,35 @@ class SnapAllyReporter {
337
402
  break;
338
403
  }
339
404
  this.executionSummary.total++;
340
- // Create color config for template
341
- const colors = {
342
- critical: ((_g = this.options.colors) === null || _g === void 0 ? void 0 : _g.critical) || '#c92a2a',
343
- serious: ((_h = this.options.colors) === null || _h === void 0 ? void 0 : _h.serious) || '#e67700',
344
- moderate: ((_j = this.options.colors) === null || _j === void 0 ? void 0 : _j.moderate) || '#ca8a04',
345
- minor: ((_k = this.options.colors) === null || _k === void 0 ? void 0 : _k.minor) || '#0891b2',
346
- };
347
- // Render Step Report
348
- const indexFile = path.join(testResultsFolder, `execution-${sanitizedTitle}.html`);
349
- await this.renderer.render('test-execution-report.html', { ...testStats, colors }, testResultsFolder, indexFile);
350
405
  }
351
- async onEnd(result) {
352
- var _a, _b, _c, _d;
353
- // Wait for all test result processing to finish
354
- await Promise.all(this.tasks);
355
- const summaryFile = path.join(this.outputFolder, 'summary.html');
356
- this.executionSummary.duration = A11yTimeUtils_1.A11yTimeUtils.formatDuration(result.duration);
357
- this.executionSummary.status = result.status;
358
- this.executionSummary.statusIcon =
359
- models_1.TestStatusIcon[result.status] || 'help';
360
- this.executionSummary.date = A11yTimeUtils_1.A11yTimeUtils.formatDate(new Date());
361
- const colors = {
362
- critical: ((_a = this.options.colors) === null || _a === void 0 ? void 0 : _a.critical) || '#c92a2a',
363
- serious: ((_b = this.options.colors) === null || _b === void 0 ? void 0 : _b.serious) || '#e67700',
364
- moderate: ((_c = this.options.colors) === null || _c === void 0 ? void 0 : _c.moderate) || '#ca8a04',
365
- minor: ((_d = this.options.colors) === null || _d === void 0 ? void 0 : _d.minor) || '#0891b2',
366
- };
367
- await this.renderer.render('execution-summary.html', { ...this.executionSummary, colors }, this.outputFolder, summaryFile);
368
- console.log(`\n[SnapAlly] Reports generated in: ${path.resolve(this.outputFolder)}`);
406
+ // ────────────────────────────────────────────────────────────────────────
407
+ // Helpers
408
+ // ────────────────────────────────────────────────────────────────────────
409
+ /** Ensures a file group key exists in the grouped results map. */
410
+ ensureGroupExists(fileGroup) {
411
+ if (!this.executionSummary.groupedResults[fileGroup]) {
412
+ this.executionSummary.groupedResults[fileGroup] = [];
413
+ }
414
+ }
415
+ /** Lazily initialises and returns the browser summary for the given browser name. */
416
+ getOrCreateBrowserSummary(browser) {
417
+ const summaries = this.executionSummary.browserSummaries;
418
+ if (!summaries[browser]) {
419
+ summaries[browser] = {
420
+ duration: '0s',
421
+ status: '',
422
+ statusIcon: '',
423
+ total: 0,
424
+ totalFailed: 0,
425
+ totalFlaky: 0,
426
+ totalPassed: 0,
427
+ totalSkipped: 0,
428
+ groupedResults: {},
429
+ wcagErrors: {},
430
+ totalA11yErrorCount: 0,
431
+ };
432
+ }
433
+ return summaries[browser];
369
434
  }
370
435
  }
371
436
  exports.default = SnapAllyReporter;