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,180 @@
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
+ exports.A11yReportAssets = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ /**
40
+ * Utilities for managing and copying report assets like videos and screenshots.
41
+ */
42
+ class A11yReportAssets {
43
+ /**
44
+ * Copies a file from source to a destination folder.
45
+ */
46
+ copyToFolder(destFolder, srcPath, fileName) {
47
+ if (!srcPath || !fs.existsSync(srcPath)) {
48
+ return '';
49
+ }
50
+ const name = fileName || path.basename(srcPath);
51
+ const destFile = path.join(destFolder, name);
52
+ if (!fs.existsSync(destFolder)) {
53
+ fs.mkdirSync(destFolder, { recursive: true });
54
+ }
55
+ fs.copyFileSync(srcPath, destFile);
56
+ return name;
57
+ }
58
+ /**
59
+ * Copies the test video if available.
60
+ * Includes a small retry to ensure Playwright has finished flushing the file.
61
+ */
62
+ async copyTestVideo(result, destFolder) {
63
+ const videoAttachments = result.attachments.filter(a => a.name === 'video');
64
+ let bestVideo = null;
65
+ let maxSize = -1;
66
+ for (const attachment of videoAttachments) {
67
+ if (!attachment.path)
68
+ continue;
69
+ // Retry logic: Wait for file to exist and have non-zero size (up to 2 seconds)
70
+ let attempts = 0;
71
+ let isReady = false;
72
+ while (attempts < 10) {
73
+ if (fs.existsSync(attachment.path)) {
74
+ try {
75
+ if (fs.statSync(attachment.path).size > 0) {
76
+ isReady = true;
77
+ break;
78
+ }
79
+ }
80
+ catch (e) {
81
+ // statSync might fail if file is temporarily locked
82
+ }
83
+ }
84
+ await new Promise(r => setTimeout(r, 200));
85
+ attempts++;
86
+ }
87
+ if (isReady) {
88
+ try {
89
+ const size = fs.statSync(attachment.path).size;
90
+ if (size > maxSize) {
91
+ maxSize = size;
92
+ bestVideo = attachment.path;
93
+ }
94
+ }
95
+ catch (err) {
96
+ console.error(`[SnapAlly] Error checking video stats: ${err}`);
97
+ }
98
+ }
99
+ else {
100
+ console.warn(`[SnapAlly] Video attachment found but file is missing or empty: ${attachment.path}`);
101
+ }
102
+ }
103
+ if (bestVideo) {
104
+ try {
105
+ return this.copyToFolder(destFolder, bestVideo);
106
+ }
107
+ catch (e) {
108
+ console.error(`[SnapAlly] Failed to copy video: ${e}`);
109
+ return path.basename(bestVideo);
110
+ }
111
+ }
112
+ return '';
113
+ }
114
+ /**
115
+ * Copies all screenshots found in the test attachments.
116
+ */
117
+ copyScreenshots(result, destFolder) {
118
+ return result.attachments
119
+ .filter(a => a.name === 'screenshot')
120
+ .map(a => {
121
+ if (a.path) {
122
+ return this.copyToFolder(destFolder, a.path);
123
+ }
124
+ else if (a.body) {
125
+ return this.writeBuffer(destFolder, `screenshot-${Date.now()}.png`, a.body);
126
+ }
127
+ return '';
128
+ })
129
+ .filter(path => path !== '');
130
+ }
131
+ /**
132
+ * Copies all PNG attachments to the report folder and returns their new names.
133
+ */
134
+ copyPngAttachments(result, destFolder) {
135
+ return result.attachments
136
+ .filter(a => a.name.endsWith('.png') && a.name !== 'screenshot')
137
+ .map(a => {
138
+ let name = '';
139
+ if (a.path) {
140
+ name = this.copyToFolder(destFolder, a.path, a.name);
141
+ }
142
+ else if (a.body) {
143
+ name = this.writeBuffer(destFolder, a.name, a.body);
144
+ }
145
+ return name ? { path: name, name: a.name } : null;
146
+ })
147
+ .filter((item) => item !== null);
148
+ }
149
+ /**
150
+ * Copies all other attachments (traces, logs, etc.) to the report folder.
151
+ */
152
+ copyAllOtherAttachments(result, destFolder) {
153
+ const excludedNames = ['screenshot', 'video', 'A11y'];
154
+ return result.attachments
155
+ .filter(a => !excludedNames.includes(a.name) && !a.name.endsWith('.png'))
156
+ .map(a => {
157
+ let name = '';
158
+ if (a.path) {
159
+ name = this.copyToFolder(destFolder, a.path, a.name);
160
+ }
161
+ else if (a.body) {
162
+ name = this.writeBuffer(destFolder, a.name, a.body);
163
+ }
164
+ return name ? { path: name, name: a.name } : null;
165
+ })
166
+ .filter((item) => item !== null);
167
+ }
168
+ /**
169
+ * Writes a buffer to a file in the destination folder.
170
+ */
171
+ writeBuffer(destFolder, fileName, buffer) {
172
+ if (!fs.existsSync(destFolder)) {
173
+ fs.mkdirSync(destFolder, { recursive: true });
174
+ }
175
+ const destFile = path.join(destFolder, fileName);
176
+ fs.writeFileSync(destFile, buffer);
177
+ return fileName;
178
+ }
179
+ }
180
+ exports.A11yReportAssets = A11yReportAssets;
@@ -0,0 +1,25 @@
1
+ import { Page, Locator, TestInfo } from '@playwright/test';
2
+ export interface A11yScannerOptions {
3
+ /** Specific selector or locator to include in the scan. */
4
+ include?: string | Locator;
5
+ /** Alias for include. */
6
+ box?: string | Locator;
7
+ /** Whether to log violations to the console. @default true */
8
+ verbose?: boolean;
9
+ /** Alias for verbose. */
10
+ consoleLog?: boolean;
11
+ /** Specific Axe rules to enable or disable. */
12
+ rules?: Record<string, {
13
+ enabled: boolean;
14
+ }>;
15
+ /** Specific WCAG tags to check (e.g., ['wcag2a', 'wcag2aa']). */
16
+ tags?: string[];
17
+ /** Any other Axe-core options to pass to the builder. */
18
+ axeOptions?: Record<string, unknown>;
19
+ }
20
+ /**
21
+ * Performs an accessibility audit using Axe and Lighthouse.
22
+ */
23
+ export declare function scanA11y(page: Page, testInfo: TestInfo, options?: A11yScannerOptions): Promise<void>;
24
+ /** Alias for backward compatibility */
25
+ export declare const checkAccessibility: typeof scanA11y;
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkAccessibility = void 0;
7
+ exports.scanA11y = scanA11y;
8
+ const playwright_1 = __importDefault(require("@axe-core/playwright"));
9
+ const test_1 = require("@playwright/test");
10
+ const A11yAuditOverlay_1 = require("./A11yAuditOverlay");
11
+ const models_1 = require("./models");
12
+ /**
13
+ * Performs an accessibility audit using Axe and Lighthouse.
14
+ */
15
+ async function scanA11y(page, testInfo, options = {}) {
16
+ var _a;
17
+ const verbose = (_a = options.verbose) !== null && _a !== void 0 ? _a : true;
18
+ const overlay = new A11yAuditOverlay_1.A11yAuditOverlay(page, page.url());
19
+ // Configure Axe
20
+ let axeBuilder = new playwright_1.default({ page });
21
+ const target = options.include || options.box;
22
+ if (target) {
23
+ if (typeof target === 'string') {
24
+ axeBuilder = axeBuilder.include(target);
25
+ }
26
+ else {
27
+ // AxeBuilder for playwright also supports locators/elements in include
28
+ axeBuilder = axeBuilder.include(target);
29
+ }
30
+ }
31
+ if (options.rules) {
32
+ axeBuilder = axeBuilder.options({ rules: options.rules });
33
+ }
34
+ if (options.tags) {
35
+ axeBuilder = axeBuilder.withTags(options.tags);
36
+ }
37
+ if (options.axeOptions) {
38
+ axeBuilder = axeBuilder.options(options.axeOptions);
39
+ }
40
+ const axeResults = await axeBuilder.analyze();
41
+ const violationCount = axeResults.violations.length;
42
+ if (verbose && violationCount > 0) {
43
+ console.log(`\n[A11yScanner] Violations found: ${violationCount}`);
44
+ axeResults.violations.forEach((v, i) => {
45
+ console.log(` ${i + 1}. ${v.id} [${v.impact}] - ${v.help}`);
46
+ });
47
+ }
48
+ // Fail the test if violations found (softly)
49
+ test_1.expect.soft(violationCount, `Accessibility audit failed with ${violationCount} violations.`).toBe(0);
50
+ // Run Axe Audit
51
+ const errors = [];
52
+ const colorMap = {
53
+ minor: '#0ea5e9', // Ocean Blue
54
+ moderate: '#f59e0b', // Amber/Honey
55
+ serious: '#ea580c', // Deep Orange
56
+ critical: '#dc2626' // Power Red
57
+ };
58
+ // Process violations for the report
59
+ for (const violation of axeResults.violations) {
60
+ let errorIdx = 0;
61
+ const targets = [];
62
+ const severityColor = colorMap[violation.impact || ''] || '#757575';
63
+ for (const node of violation.nodes) {
64
+ for (const selector of node.target) {
65
+ const elementSelector = selector.toString();
66
+ const locator = page.locator(elementSelector);
67
+ await overlay.showViolationOverlay({ id: violation.id, help: violation.help }, severityColor);
68
+ if (await locator.isVisible()) {
69
+ await overlay.highlightElement(elementSelector, severityColor);
70
+ // Allow time for video capture or manual inspection during debug
71
+ // eslint-disable-next-line playwright/no-wait-for-timeout
72
+ await page.waitForTimeout(2000);
73
+ const screenshotName = `a11y-${violation.id}-${errorIdx++}.png`;
74
+ const buffer = await overlay.captureAndAttachScreenshot(screenshotName, testInfo);
75
+ // Capture execution steps for context
76
+ const excluded = new Set(['Pre Condition', 'Post Condition', 'Description', 'A11y']);
77
+ const contextSteps = (testInfo.annotations || [])
78
+ .filter(a => !excluded.has(a.type))
79
+ .map(a => a.description || '');
80
+ const nodeHtml = node.html || '';
81
+ const friendlySnippet = elementSelector; // Use full CSS selector path from Axe core
82
+ targets.push({
83
+ element: elementSelector,
84
+ snippet: friendlySnippet,
85
+ html: nodeHtml,
86
+ screenshot: screenshotName,
87
+ steps: contextSteps,
88
+ stepsJson: JSON.stringify(contextSteps),
89
+ screenshotBase64: buffer.toString('base64')
90
+ });
91
+ await overlay.unhighlightElement();
92
+ }
93
+ }
94
+ }
95
+ errors.push({
96
+ id: violation.id,
97
+ description: violation.description,
98
+ severity: violation.impact || 'unknown',
99
+ helpUrl: violation.helpUrl,
100
+ help: violation.help,
101
+ guideline: violation.tags[1] || 'N/A',
102
+ wcagRule: violation.tags.find(t => t.startsWith('wcag')) || violation.tags[1] || 'N/A',
103
+ total: targets.length || violation.nodes.length, // Fallback to node count if no screenshots
104
+ target: targets
105
+ });
106
+ }
107
+ // Prepare data for the reporter
108
+ const reportData = {
109
+ pageKey: page.url(),
110
+ accessibilityScore: 0, // No longer used, derivation from Lighthouse removed
111
+ errors,
112
+ video: 'a11y-scan-video.webm', // Reference name for reporter
113
+ criticalColor: models_1.Severity.critical,
114
+ seriousColor: models_1.Severity.serious,
115
+ moderateColor: models_1.Severity.moderate,
116
+ minorColor: models_1.Severity.minor,
117
+ adoOrganization: process.env.ADO_ORGANIZATION || '',
118
+ adoProject: process.env.ADO_PROJECT || ''
119
+ };
120
+ await overlay.addTestAttachment(testInfo, 'A11y', JSON.stringify(reportData));
121
+ await overlay.hideViolationOverlay();
122
+ }
123
+ /** Alias for backward compatibility */
124
+ exports.checkAccessibility = scanA11y;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Time utility functions for formatting test durations.
3
+ */
4
+ export declare class A11yTimeUtils {
5
+ /**
6
+ * Formats milliseconds into a human-readable duration string.
7
+ */
8
+ static formatDuration(ms: number): string;
9
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.A11yTimeUtils = void 0;
4
+ /**
5
+ * Time utility functions for formatting test durations.
6
+ */
7
+ class A11yTimeUtils {
8
+ /**
9
+ * Formats milliseconds into a human-readable duration string.
10
+ */
11
+ static formatDuration(ms) {
12
+ if (ms < 1000) {
13
+ return `${ms.toFixed(0)}ms`;
14
+ }
15
+ const seconds = ms / 1000;
16
+ if (seconds < 60) {
17
+ return `${seconds.toFixed(1)}s`;
18
+ }
19
+ const minutes = Math.floor(seconds / 60);
20
+ const remainingSeconds = seconds % 60;
21
+ return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
22
+ }
23
+ }
24
+ exports.A11yTimeUtils = A11yTimeUtils;
@@ -0,0 +1,30 @@
1
+ import { Reporter, TestCase, TestResult, FullResult, FullConfig } from '@playwright/test/reporter';
2
+ import { TestSummary } from './models';
3
+ export interface AccessibilityReporterOptions {
4
+ outputFolder?: string;
5
+ colors?: {
6
+ critical?: string;
7
+ serious?: string;
8
+ moderate?: string;
9
+ minor?: string;
10
+ };
11
+ ado?: {
12
+ organization?: string;
13
+ project?: string;
14
+ };
15
+ }
16
+ declare class SnapAllyLegacyReporter implements Reporter {
17
+ private testNo;
18
+ private folderResults;
19
+ private fileHelper;
20
+ private htmlHelper;
21
+ private options;
22
+ private testDir;
23
+ summary: TestSummary;
24
+ constructor(options?: AccessibilityReporterOptions);
25
+ onBegin(config: FullConfig): void;
26
+ onTestEnd(test: TestCase, result: TestResult): Promise<void>;
27
+ onEnd(result: FullResult): Promise<void>;
28
+ private getTestSteps;
29
+ }
30
+ export default SnapAllyLegacyReporter;
@@ -0,0 +1,235 @@
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
+ class SnapAllyLegacyReporter {
42
+ constructor(options = {}) {
43
+ this.testNo = 0;
44
+ this.fileHelper = new A11yReportAssets_1.A11yReportAssets();
45
+ this.htmlHelper = new A11yHtmlRenderer_1.A11yHtmlRenderer();
46
+ this.testDir = 'tests';
47
+ // Summary state
48
+ this.summary = {
49
+ duration: '',
50
+ status: '',
51
+ statusIcon: '',
52
+ total: 0,
53
+ totalFailed: 0,
54
+ totalFlaky: 0,
55
+ totalPassed: 0,
56
+ totalSkipped: 0,
57
+ groupedResults: {},
58
+ wcagErrors: {},
59
+ totalA11yErrorCount: 0
60
+ };
61
+ this.options = options;
62
+ this.folderResults = options.outputFolder || 'steps-report';
63
+ }
64
+ onBegin(config) {
65
+ this.testDir = config.rootDir || 'tests';
66
+ }
67
+ async onTestEnd(test, result) {
68
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
69
+ this.testNo++;
70
+ const folderTest = path.join(this.folderResults, this.testNo.toString());
71
+ // --- Step Reporting Logic ---
72
+ const groupKey = path.relative(this.testDir, test.location.file);
73
+ if (!this.summary.groupedResults[groupKey]) {
74
+ this.summary.groupedResults[groupKey] = [];
75
+ }
76
+ const tags = (_a = test.tags.map(tag => tag.replace('@', ''))) !== null && _a !== void 0 ? _a : [];
77
+ const statusIcon = models_1.TestStatusIcon[result.status];
78
+ // Parse annotations for status report
79
+ const descriptionAnnotation = test.annotations.find(annotation => annotation.type == 'Description');
80
+ const description = (_b = descriptionAnnotation === null || descriptionAnnotation === void 0 ? void 0 : descriptionAnnotation.description) !== null && _b !== void 0 ? _b : 'No Description';
81
+ const browser = (_d = (_c = test.parent.project()) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : 'No browser';
82
+ // Steps filtering (exclude internal ones)
83
+ const excludedSteps = new Set(['Pre Condition', 'Post Condition', 'Description', 'A11y']);
84
+ const steps = test.annotations
85
+ .filter(annotation => !excludedSteps.has(annotation.type))
86
+ .map(annotation => { var _a; return (_a = annotation.description) !== null && _a !== void 0 ? _a : 'No steps'; });
87
+ const preConditions = test.annotations.filter(annotation => annotation.type == 'Pre Condition')
88
+ .map(annotation => { var _a; return (_a = annotation.description) !== null && _a !== void 0 ? _a : 'No pre conditions'; });
89
+ const postConditions = test.annotations.filter(annotation => annotation.type == 'Post Condition')
90
+ .map(annotation => { var _a; return (_a = annotation.description) !== null && _a !== void 0 ? _a : 'No post conditions'; });
91
+ const attachments = (_e = result.attachments
92
+ .filter(attachment => attachment.name !== 'screenshot' && attachment.name !== 'video' && !attachment.name.toLowerCase().includes('allure'))
93
+ .map(attachment => { var _a, _b; return ({ path: (_a = attachment.path) !== null && _a !== void 0 ? _a : '', name: (_b = attachment.name) !== null && _b !== void 0 ? _b : '' }); })) !== null && _e !== void 0 ? _e : [];
94
+ const reportAttachments = attachments.map(attachment => ({
95
+ path: this.fileHelper.copyToFolder(folderTest, attachment.path),
96
+ name: attachment.name
97
+ }));
98
+ const videoPath = await this.fileHelper.copyTestVideo(result, folderTest);
99
+ const screenshotPaths = this.fileHelper.copyScreenshots(result, folderTest);
100
+ const errors = (_f = result.errors.map(error => { var _a; return this.htmlHelper.ansiToHtml((_a = error.message) !== null && _a !== void 0 ? _a : 'No errors'); })) !== null && _f !== void 0 ? _f : [];
101
+ const resultItem = {
102
+ num: this.testNo,
103
+ folderName: this.testNo.toString(),
104
+ title: test.title,
105
+ fileName: groupKey,
106
+ timeDuration: result.duration,
107
+ duration: A11yTimeUtils_1.A11yTimeUtils.formatDuration(result.duration),
108
+ description: description,
109
+ status: result.status,
110
+ browser: browser,
111
+ tags: tags,
112
+ preConditions: preConditions,
113
+ steps: steps,
114
+ postConditions: postConditions,
115
+ statusIcon: statusIcon,
116
+ videoPath: videoPath,
117
+ screenshotPaths: screenshotPaths,
118
+ attachments: reportAttachments,
119
+ errors: errors
120
+ };
121
+ this.summary.groupedResults[groupKey].push(resultItem);
122
+ const wasRetried = test.results && test.results.length > 1;
123
+ const isFlaky = wasRetried && result.status === 'passed';
124
+ if (isFlaky)
125
+ this.summary.totalFlaky++;
126
+ switch (result.status) {
127
+ case 'passed':
128
+ this.summary.totalPassed++;
129
+ break;
130
+ case 'failed':
131
+ this.summary.totalFailed++;
132
+ break;
133
+ case 'skipped':
134
+ this.summary.totalSkipped++;
135
+ break;
136
+ }
137
+ this.summary.total++;
138
+ // Generate Step Report (index.html)
139
+ const indexFilePath = path.join(folderTest, 'index.html');
140
+ await this.htmlHelper.render('stepReporter.html', { result: resultItem }, folderTest, indexFilePath);
141
+ // --- Accessibility Reporting Logic ---
142
+ // Only process if there is A11y annotation
143
+ const reportDataAnnotation = test.annotations.find(annotation => annotation.type === 'A11y');
144
+ if (reportDataAnnotation) {
145
+ let fileName = `a11y${this.testNo}.html`;
146
+ let reportData = JSON.parse((_g = reportDataAnnotation.description) !== null && _g !== void 0 ? _g : '{}');
147
+ // Sanitize pageKey for filename
148
+ if (reportData.pageKey) {
149
+ const sanitizedKey = reportData.pageKey
150
+ .replace(/https?:\/\//, '')
151
+ .replace(/[^a-z0-9]+/gi, '-')
152
+ .replace(/^-+|-+$/g, '')
153
+ .toLowerCase();
154
+ fileName = sanitizedKey ? `${sanitizedKey}.html` : fileName;
155
+ }
156
+ const filePath = path.join(folderTest, fileName);
157
+ // Override configs with options or defaults
158
+ reportData.criticalColor = ((_h = this.options.colors) === null || _h === void 0 ? void 0 : _h.critical) || '#bd1f35';
159
+ reportData.seriousColor = ((_j = this.options.colors) === null || _j === void 0 ? void 0 : _j.serious) || '#d67f05';
160
+ reportData.moderateColor = ((_k = this.options.colors) === null || _k === void 0 ? void 0 : _k.moderate) || '#f0c000';
161
+ reportData.minorColor = ((_l = this.options.colors) === null || _l === void 0 ? void 0 : _l.minor) || '#2da4cf';
162
+ if (this.options.ado) {
163
+ if (this.options.ado.organization)
164
+ reportData.adoOrganization = this.options.ado.organization;
165
+ if (this.options.ado.project)
166
+ reportData.adoProject = this.options.ado.project;
167
+ }
168
+ // Enrich with Playwright steps if available
169
+ const playwrightSteps = this.getTestSteps(result);
170
+ if (playwrightSteps.length > 0 && reportData.errors) {
171
+ reportData.errors.forEach(error => {
172
+ if (error.target) {
173
+ error.target.forEach(target => {
174
+ // Merge annotation steps with Playwright steps
175
+ const existingSteps = new Set(target.steps || []);
176
+ playwrightSteps.forEach(step => {
177
+ if (!existingSteps.has(step)) {
178
+ if (!target.steps)
179
+ target.steps = [];
180
+ target.steps.push(step);
181
+ }
182
+ });
183
+ // Update JSON for bug creation
184
+ target.stepsJson = JSON.stringify(target.steps);
185
+ });
186
+ }
187
+ // Aggregate WCAG errors for summary chart
188
+ const ruleId = error.id;
189
+ const totalViolations = error.total || 0;
190
+ if (!this.summary.wcagErrors[ruleId]) {
191
+ this.summary.wcagErrors[ruleId] = { count: 0, severity: error.severity, helpUrl: error.helpUrl };
192
+ }
193
+ this.summary.wcagErrors[ruleId].count += totalViolations;
194
+ });
195
+ }
196
+ this.fileHelper.copyPngAttachments(result, folderTest);
197
+ await this.htmlHelper.render('page-report.html', { data: reportData, folderTest }, folderTest, filePath);
198
+ }
199
+ }
200
+ async onEnd(result) {
201
+ var _a, _b, _c, _d;
202
+ const folderTest = this.folderResults;
203
+ const summaryName = 'index.html';
204
+ const summaryPath = path.join(folderTest, summaryName);
205
+ this.summary.duration = A11yTimeUtils_1.A11yTimeUtils.formatDuration(result.duration);
206
+ this.summary.status = result.status;
207
+ const statusIcon = models_1.TestStatusIcon[result.status];
208
+ this.summary.statusIcon = statusIcon;
209
+ const colors = {
210
+ critical: ((_a = this.options.colors) === null || _a === void 0 ? void 0 : _a.critical) || '#bd1f35',
211
+ serious: ((_b = this.options.colors) === null || _b === void 0 ? void 0 : _b.serious) || '#d67f05',
212
+ moderate: ((_c = this.options.colors) === null || _c === void 0 ? void 0 : _c.moderate) || '#f0c000',
213
+ minor: ((_d = this.options.colors) === null || _d === void 0 ? void 0 : _d.minor) || '#2da4cf'
214
+ };
215
+ await this.htmlHelper.render('global-summary.html', { results: this.summary, colors }, folderTest, summaryPath);
216
+ }
217
+ getTestSteps(result) {
218
+ const steps = [];
219
+ const processSteps = (testSteps) => {
220
+ for (const step of testSteps) {
221
+ if (step.category === 'test.step') {
222
+ steps.push(step.title);
223
+ }
224
+ if (step.steps) {
225
+ processSteps(step.steps);
226
+ }
227
+ }
228
+ };
229
+ if (result.steps) {
230
+ processSteps(result.steps);
231
+ }
232
+ return steps;
233
+ }
234
+ }
235
+ exports.default = SnapAllyLegacyReporter;
@@ -0,0 +1,44 @@
1
+ import { Reporter, TestCase, TestResult, FullResult, FullConfig } from '@playwright/test/reporter';
2
+ export interface AccessibilityReporterOptions {
3
+ /**
4
+ * Folder where the reports will be generated.
5
+ * @default "steps-report"
6
+ */
7
+ outputFolder?: string;
8
+ /**
9
+ * Custom colors for violation severities in the report.
10
+ */
11
+ colors?: {
12
+ critical?: string;
13
+ serious?: string;
14
+ moderate?: string;
15
+ minor?: string;
16
+ };
17
+ /**
18
+ * Azure DevOps integration options.
19
+ */
20
+ ado?: {
21
+ organization?: string;
22
+ project?: string;
23
+ };
24
+ }
25
+ /**
26
+ * Playwright reporter for accessibility audits and test steps.
27
+ * Generates an execution summary and detailed reports per test.
28
+ */
29
+ declare class SnapAllyReporter implements Reporter {
30
+ private testIndex;
31
+ private outputFolder;
32
+ private assetsManager;
33
+ private renderer;
34
+ private options;
35
+ private projectRoot;
36
+ private executionSummary;
37
+ private tasks;
38
+ constructor(options?: AccessibilityReporterOptions);
39
+ onBegin(config: FullConfig): void;
40
+ onTestEnd(test: TestCase, result: TestResult): void;
41
+ private processTestResult;
42
+ onEnd(result: FullResult): Promise<void>;
43
+ }
44
+ export default SnapAllyReporter;