snap-ally 0.0.1

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.
@@ -0,0 +1,332 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
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
+ const fs = __importStar(require("fs"));
42
+ /**
43
+ * Playwright reporter for accessibility audits and test steps.
44
+ * Generates an execution summary and detailed reports per test.
45
+ */
46
+ class SnapAllyReporter {
47
+ constructor(options = {}) {
48
+ this.testIndex = 0;
49
+ this.assetsManager = new A11yReportAssets_1.A11yReportAssets();
50
+ this.renderer = new A11yHtmlRenderer_1.A11yHtmlRenderer();
51
+ this.projectRoot = 'tests';
52
+ // Global summary tracking
53
+ this.executionSummary = {
54
+ duration: '',
55
+ status: '',
56
+ statusIcon: '',
57
+ total: 0,
58
+ totalFailed: 0,
59
+ totalFlaky: 0,
60
+ totalPassed: 0,
61
+ totalSkipped: 0,
62
+ groupedResults: {},
63
+ wcagErrors: {},
64
+ totalA11yErrorCount: 0,
65
+ browserSummaries: {}
66
+ };
67
+ // Track async tasks to ensure they finish before onEnd
68
+ this.tasks = [];
69
+ this.options = options;
70
+ this.outputFolder = path.resolve(process.cwd(), options.outputFolder || 'steps-report');
71
+ }
72
+ onBegin(config) {
73
+ this.projectRoot = config.rootDir || 'tests';
74
+ }
75
+ onTestEnd(test, result) {
76
+ this.tasks.push(this.processTestResult(test, result));
77
+ }
78
+ async processTestResult(test, result) {
79
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
80
+ this.testIndex++;
81
+ const sanitizedTitle = test.title.replace(/[^a-z0-9]+/gi, '-').toLowerCase();
82
+ const testFolderName = `${this.testIndex}-${sanitizedTitle}`;
83
+ const testResultsFolder = path.join(this.outputFolder, testFolderName);
84
+ // --- 1. Functional Step Reporting ---
85
+ const fileGroup = path.relative(this.projectRoot, test.location.file);
86
+ if (!this.executionSummary.groupedResults[fileGroup]) {
87
+ this.executionSummary.groupedResults[fileGroup] = [];
88
+ }
89
+ const tags = test.tags.map(t => t.replace('@', ''));
90
+ const statusIcon = models_1.TestStatusIcon[result.status] || 'help';
91
+ const browser = ((_a = test.parent.project()) === null || _a === void 0 ? void 0 : _a.name) || 'unknown';
92
+ const descAnnotation = test.annotations.find(a => a.type === 'Description');
93
+ const description = (descAnnotation === null || descAnnotation === void 0 ? void 0 : descAnnotation.description) || 'No Description';
94
+ // Prepare steps from annotations
95
+ const skipTypes = new Set(['Pre Condition', 'Post Condition', 'Description', 'A11y']);
96
+ const steps = test.annotations
97
+ .filter(a => !skipTypes.has(a.type))
98
+ .map(a => a.description || 'Step');
99
+ const preConditions = test.annotations
100
+ .filter(a => a.type === 'Pre Condition')
101
+ .map(a => a.description || '');
102
+ const postConditions = test.annotations
103
+ .filter(a => a.type === 'Post Condition')
104
+ .map(a => a.description || '');
105
+ const video = await this.assetsManager.copyTestVideo(result, testResultsFolder);
106
+ const screenshots = this.assetsManager.copyScreenshots(result, testResultsFolder);
107
+ const pngAttachments = this.assetsManager.copyPngAttachments(result, testResultsFolder);
108
+ const otherAttachments = this.assetsManager.copyAllOtherAttachments(result, testResultsFolder);
109
+ const allAttachments = [...pngAttachments, ...otherAttachments];
110
+ console.log(`[SnapAlly Debug] Test "${test.title}" ended. Status: ${result.status}. Video: ${video ? 'Created' : 'Missing'}`);
111
+ console.log(`[SnapAlly Debug] Raw Attachments: ${result.attachments.map(a => `${a.name} (${a.path ? 'file' : 'body'})`).join(', ')}`);
112
+ const errorLogs = (result.errors || []).map(err => {
113
+ const fullMsg = err.stack ? `${err.message}\n${err.stack}` : (err.message || 'Error occurred');
114
+ return this.renderer.ansiToHtml(fullMsg);
115
+ }) || [];
116
+ // --- 2. Accessibility Reporting (Iterate over all A11y sources: attachments and annotations) ---
117
+ const a11yAttachments = (result.attachments || []).filter(a => a.name === 'A11y');
118
+ const a11yAnnotations = (test.annotations || []).filter(a => a.type === 'A11y');
119
+ const a11yDataSources = [
120
+ ...a11yAttachments.map((a) => ({ type: 'attachment', data: a })),
121
+ ...a11yAnnotations.map((a) => ({ type: 'annotation', data: a }))
122
+ ];
123
+ if (a11yDataSources.length === 0) {
124
+ console.error(`[SnapAlly Debug] No A11y data sources found for test: ${test.title}`);
125
+ }
126
+ let a11yReportPath = undefined;
127
+ let a11yErrorCount = 0;
128
+ let aggregatedA11yErrors = [];
129
+ // Loop through all accessibility scans in this test
130
+ for (const [index, source] of a11yDataSources.entries()) {
131
+ let reportData;
132
+ try {
133
+ if (source.type === 'attachment') {
134
+ const attach = source.data;
135
+ if (attach.body) {
136
+ reportData = JSON.parse(attach.body.toString());
137
+ }
138
+ else if (attach.path && fs.existsSync(attach.path)) {
139
+ reportData = JSON.parse(fs.readFileSync(attach.path, 'utf-8'));
140
+ }
141
+ else {
142
+ continue;
143
+ }
144
+ }
145
+ else {
146
+ const annot = source.data;
147
+ reportData = JSON.parse(annot.description || '{}');
148
+ }
149
+ }
150
+ catch (e) {
151
+ console.error(`[SnapAlly] Failed to parse A11y ${source.type}: ${e}`);
152
+ errorLogs.push(this.renderer.ansiToHtml(`[SnapAlly] Internal error parsing accessibility data from ${source.type}: ${e}`));
153
+ continue;
154
+ }
155
+ // Determine Report Name (append index if multiple)
156
+ let a11yReportName = `accessibility-${sanitizedTitle}.html`;
157
+ if (a11yDataSources.length > 1) {
158
+ a11yReportName = `accessibility-${sanitizedTitle}-${index + 1}.html`;
159
+ }
160
+ // Sanitize pageKey for filename override if present
161
+ if (reportData.pageKey) {
162
+ const sanitizedKey = reportData.pageKey
163
+ .replace(/https?:\/\//, '')
164
+ .replace(/[^a-z0-9]+/gi, '-')
165
+ .replace(/^-+|-+$/g, '')
166
+ .toLowerCase();
167
+ if (sanitizedKey) {
168
+ a11yReportName = a11yDataSources.length > 1
169
+ ? `${sanitizedKey}-${index + 1}.html`
170
+ : `${sanitizedKey}.html`;
171
+ }
172
+ }
173
+ // Set the main report path to the LAST one (or maybe first? using last for now)
174
+ a11yReportPath = a11yReportName;
175
+ // Re-apply configuration
176
+ reportData.criticalColor = ((_b = this.options.colors) === null || _b === void 0 ? void 0 : _b.critical) || '#c92a2a';
177
+ reportData.seriousColor = ((_c = this.options.colors) === null || _c === void 0 ? void 0 : _c.serious) || '#e67700';
178
+ reportData.moderateColor = ((_d = this.options.colors) === null || _d === void 0 ? void 0 : _d.moderate) || '#ca8a04';
179
+ reportData.minorColor = ((_e = this.options.colors) === null || _e === void 0 ? void 0 : _e.minor) || '#0891b2';
180
+ if (this.options.ado) {
181
+ reportData.adoOrganization = this.options.ado.organization || reportData.adoOrganization;
182
+ reportData.adoProject = this.options.ado.project || reportData.adoProject;
183
+ }
184
+ // Sync video name
185
+ if (video)
186
+ reportData.video = video;
187
+ const auditFile = path.join(testResultsFolder, a11yReportName);
188
+ await this.renderer.render('accessibility-report.html', { data: reportData, folderTest: testResultsFolder }, testResultsFolder, auditFile);
189
+ // --- 3. Update Browser-Specific Summary (Partial Aggregation) ---
190
+ if (!this.executionSummary.browserSummaries[browser]) {
191
+ this.executionSummary.browserSummaries[browser] = {
192
+ duration: '0s',
193
+ status: '',
194
+ statusIcon: '',
195
+ total: 0,
196
+ totalFailed: 0,
197
+ totalFlaky: 0,
198
+ totalPassed: 0,
199
+ totalSkipped: 0,
200
+ groupedResults: {},
201
+ wcagErrors: {},
202
+ totalA11yErrorCount: 0
203
+ };
204
+ }
205
+ const bSummary = this.executionSummary.browserSummaries[browser];
206
+ if (reportData.errors && reportData.errors.length > 0) {
207
+ // Aggregate counts
208
+ const scanErrorCount = reportData.errors.reduce((sum, err) => sum + (err.total || 0), 0);
209
+ a11yErrorCount += scanErrorCount;
210
+ aggregatedA11yErrors.push(...reportData.errors);
211
+ reportData.errors.forEach((err) => {
212
+ const rule = err.id;
213
+ // Local Browser aggregation
214
+ if (!bSummary.wcagErrors[rule]) {
215
+ bSummary.wcagErrors[rule] = {
216
+ count: 0,
217
+ severity: err.severity,
218
+ helpUrl: err.helpUrl,
219
+ description: err.description
220
+ };
221
+ }
222
+ bSummary.wcagErrors[rule].count += (err.total || 0);
223
+ // Global aggregation (always add to ensure summary is not empty)
224
+ if (!this.executionSummary.wcagErrors[rule]) {
225
+ this.executionSummary.wcagErrors[rule] = {
226
+ count: 0,
227
+ severity: err.severity,
228
+ helpUrl: err.helpUrl,
229
+ description: err.description
230
+ };
231
+ }
232
+ this.executionSummary.wcagErrors[rule].count += (err.total || 0);
233
+ });
234
+ // Update total error counts
235
+ bSummary.totalA11yErrorCount += scanErrorCount;
236
+ this.executionSummary.totalA11yErrorCount += scanErrorCount;
237
+ }
238
+ }
239
+ // --- 4. Final Aggregation and Test Stats ---
240
+ // Update browser summary counts (always, even if no a11y scan occurred)
241
+ if (!this.executionSummary.browserSummaries[browser]) {
242
+ this.executionSummary.browserSummaries[browser] = {
243
+ duration: '0s', status: '', statusIcon: '', total: 0,
244
+ totalFailed: 0, totalFlaky: 0, totalPassed: 0, totalSkipped: 0,
245
+ groupedResults: {}, wcagErrors: {}, totalA11yErrorCount: 0
246
+ };
247
+ }
248
+ const bSummary = this.executionSummary.browserSummaries[browser];
249
+ bSummary.total++;
250
+ switch (result.status) {
251
+ case 'passed':
252
+ bSummary.totalPassed++;
253
+ break;
254
+ case 'failed':
255
+ bSummary.totalFailed++;
256
+ break;
257
+ case 'skipped':
258
+ bSummary.totalSkipped++;
259
+ break;
260
+ }
261
+ const executionReportName = `execution-${sanitizedTitle}.html`;
262
+ const testStats = {
263
+ num: this.testIndex,
264
+ folderName: testFolderName,
265
+ executionReportPath: `${testFolderName}/${executionReportName}`,
266
+ title: test.title,
267
+ fileName: fileGroup,
268
+ timeDuration: result.duration,
269
+ duration: A11yTimeUtils_1.A11yTimeUtils.formatDuration(result.duration),
270
+ description,
271
+ status: result.status,
272
+ browser,
273
+ tags,
274
+ preConditions,
275
+ steps,
276
+ postConditions,
277
+ statusIcon,
278
+ videoPath: video,
279
+ screenshotPaths: screenshots,
280
+ attachments: allAttachments,
281
+ errors: errorLogs,
282
+ a11yReportPath,
283
+ a11yErrorCount,
284
+ a11yErrors: aggregatedA11yErrors
285
+ };
286
+ this.executionSummary.groupedResults[fileGroup].push(testStats);
287
+ // Update summary counts
288
+ const isFlaky = test.results.length > 1 && result.status === 'passed';
289
+ if (isFlaky)
290
+ this.executionSummary.totalFlaky++;
291
+ switch (result.status) {
292
+ case 'passed':
293
+ this.executionSummary.totalPassed++;
294
+ break;
295
+ case 'failed':
296
+ this.executionSummary.totalFailed++;
297
+ break;
298
+ case 'skipped':
299
+ this.executionSummary.totalSkipped++;
300
+ break;
301
+ }
302
+ this.executionSummary.total++;
303
+ // Create color config for template
304
+ const colors = {
305
+ critical: ((_f = this.options.colors) === null || _f === void 0 ? void 0 : _f.critical) || '#c92a2a',
306
+ serious: ((_g = this.options.colors) === null || _g === void 0 ? void 0 : _g.serious) || '#e67700',
307
+ moderate: ((_h = this.options.colors) === null || _h === void 0 ? void 0 : _h.moderate) || '#ca8a04',
308
+ minor: ((_j = this.options.colors) === null || _j === void 0 ? void 0 : _j.minor) || '#0891b2'
309
+ };
310
+ // Render Step Report
311
+ const indexFile = path.join(testResultsFolder, `execution-${sanitizedTitle}.html`);
312
+ await this.renderer.render('test-execution-report.html', { result: testStats, colors }, testResultsFolder, indexFile);
313
+ }
314
+ async onEnd(result) {
315
+ var _a, _b, _c, _d;
316
+ // Wait for all test result processing to finish
317
+ await Promise.all(this.tasks);
318
+ const summaryFile = path.join(this.outputFolder, 'summary.html');
319
+ this.executionSummary.duration = A11yTimeUtils_1.A11yTimeUtils.formatDuration(result.duration);
320
+ this.executionSummary.status = result.status;
321
+ this.executionSummary.statusIcon = models_1.TestStatusIcon[result.status] || 'help';
322
+ const colors = {
323
+ critical: ((_a = this.options.colors) === null || _a === void 0 ? void 0 : _a.critical) || '#c92a2a',
324
+ serious: ((_b = this.options.colors) === null || _b === void 0 ? void 0 : _b.serious) || '#e67700',
325
+ moderate: ((_c = this.options.colors) === null || _c === void 0 ? void 0 : _c.moderate) || '#ca8a04',
326
+ minor: ((_d = this.options.colors) === null || _d === void 0 ? void 0 : _d.minor) || '#0891b2'
327
+ };
328
+ await this.renderer.render('execution-summary.html', { results: this.executionSummary, colors }, this.outputFolder, summaryFile);
329
+ console.log(`\n[SnapAlly] Reports generated in: ${path.resolve(this.outputFolder)}`);
330
+ }
331
+ }
332
+ exports.default = SnapAllyReporter;
@@ -0,0 +1,9 @@
1
+ import SnapAllyReporter from './SnapAllyReporter';
2
+ export default SnapAllyReporter;
3
+ export { scanA11y, checkAccessibility, A11yScannerOptions } from './A11yScanner';
4
+ export { AccessibilityReporterOptions } from './SnapAllyReporter';
5
+ export { A11yAuditOverlay } from './A11yAuditOverlay';
6
+ export { A11yReportAssets } from './A11yReportAssets';
7
+ export { A11yHtmlRenderer } from './A11yHtmlRenderer';
8
+ export { A11yTimeUtils } from './A11yTimeUtils';
9
+ export * from './models';
package/dist/index.js ADDED
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.A11yTimeUtils = exports.A11yHtmlRenderer = exports.A11yReportAssets = exports.A11yAuditOverlay = exports.checkAccessibility = exports.scanA11y = void 0;
21
+ const SnapAllyReporter_1 = __importDefault(require("./SnapAllyReporter"));
22
+ exports.default = SnapAllyReporter_1.default;
23
+ var A11yScanner_1 = require("./A11yScanner");
24
+ Object.defineProperty(exports, "scanA11y", { enumerable: true, get: function () { return A11yScanner_1.scanA11y; } });
25
+ Object.defineProperty(exports, "checkAccessibility", { enumerable: true, get: function () { return A11yScanner_1.checkAccessibility; } });
26
+ var A11yAuditOverlay_1 = require("./A11yAuditOverlay");
27
+ Object.defineProperty(exports, "A11yAuditOverlay", { enumerable: true, get: function () { return A11yAuditOverlay_1.A11yAuditOverlay; } });
28
+ var A11yReportAssets_1 = require("./A11yReportAssets");
29
+ Object.defineProperty(exports, "A11yReportAssets", { enumerable: true, get: function () { return A11yReportAssets_1.A11yReportAssets; } });
30
+ var A11yHtmlRenderer_1 = require("./A11yHtmlRenderer");
31
+ Object.defineProperty(exports, "A11yHtmlRenderer", { enumerable: true, get: function () { return A11yHtmlRenderer_1.A11yHtmlRenderer; } });
32
+ var A11yTimeUtils_1 = require("./A11yTimeUtils");
33
+ Object.defineProperty(exports, "A11yTimeUtils", { enumerable: true, get: function () { return A11yTimeUtils_1.A11yTimeUtils; } });
34
+ __exportStar(require("./models"), exports);
@@ -0,0 +1,102 @@
1
+ export interface ReportData {
2
+ pageKey: string;
3
+ accessibilityScore: number;
4
+ video: string;
5
+ errors: A11yError[];
6
+ criticalColor: string;
7
+ seriousColor: string;
8
+ moderateColor: string;
9
+ minorColor: string;
10
+ adoOrganization?: string;
11
+ adoProject?: string;
12
+ adoPat?: string;
13
+ }
14
+ export interface A11yError {
15
+ id: string;
16
+ description: string;
17
+ wcagRule: string;
18
+ severity: string;
19
+ help: string;
20
+ helpUrl: string;
21
+ guideline: string;
22
+ total: number;
23
+ target: Target[];
24
+ }
25
+ export interface Target {
26
+ element: string;
27
+ snippet: string;
28
+ html: string;
29
+ screenshot: string;
30
+ steps: string[];
31
+ stepsJson: string;
32
+ screenshotBase64: string;
33
+ }
34
+ export interface ImagePath {
35
+ srcPath: string;
36
+ fileName: string;
37
+ }
38
+ export declare enum Severity {
39
+ minor = "minor",
40
+ moderate = "moderate",
41
+ serious = "serious",
42
+ critical = "critical"
43
+ }
44
+ export interface TestResults {
45
+ num: number;
46
+ folderName: string;
47
+ title: string;
48
+ fileName: string;
49
+ timeDuration: number;
50
+ duration: string;
51
+ description: string;
52
+ status: string;
53
+ browser: string;
54
+ tags: string[];
55
+ preConditions: string[];
56
+ steps: string[];
57
+ postConditions: string[];
58
+ statusIcon: string;
59
+ videoPath: string | null;
60
+ screenshotPaths: string[];
61
+ attachments: {
62
+ path: string;
63
+ name: string;
64
+ }[];
65
+ errors: string[];
66
+ a11yReportPath?: string;
67
+ executionReportPath?: string;
68
+ a11yErrors?: A11yError[];
69
+ a11yErrorCount?: number;
70
+ }
71
+ export interface TestSummary {
72
+ duration: string;
73
+ status: string;
74
+ statusIcon: string;
75
+ total: number;
76
+ totalPassed: number;
77
+ totalFailed: number;
78
+ totalFlaky: number;
79
+ totalSkipped: number;
80
+ groupedResults: {
81
+ [key: string]: TestResults[];
82
+ };
83
+ wcagErrors: {
84
+ [key: string]: {
85
+ count: number;
86
+ severity: string;
87
+ helpUrl?: string;
88
+ description?: string;
89
+ };
90
+ };
91
+ totalA11yErrorCount: number;
92
+ browserSummaries?: {
93
+ [browser: string]: TestSummary;
94
+ };
95
+ }
96
+ export declare enum TestStatusIcon {
97
+ passed = "check_circle",
98
+ failed = "cancel",
99
+ skipped = "remove_circle",
100
+ timedOut = "alarm_off",
101
+ interrupted = "block"
102
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TestStatusIcon = exports.Severity = void 0;
4
+ var Severity;
5
+ (function (Severity) {
6
+ Severity["minor"] = "minor";
7
+ Severity["moderate"] = "moderate";
8
+ Severity["serious"] = "serious";
9
+ Severity["critical"] = "critical";
10
+ })(Severity || (exports.Severity = Severity = {}));
11
+ var TestStatusIcon;
12
+ (function (TestStatusIcon) {
13
+ TestStatusIcon["passed"] = "check_circle";
14
+ TestStatusIcon["failed"] = "cancel";
15
+ TestStatusIcon["skipped"] = "remove_circle";
16
+ TestStatusIcon["timedOut"] = "alarm_off";
17
+ TestStatusIcon["interrupted"] = "block";
18
+ })(TestStatusIcon || (exports.TestStatusIcon = TestStatusIcon = {}));