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.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/A11yAuditOverlay.d.ts +42 -0
- package/dist/A11yAuditOverlay.js +194 -0
- package/dist/A11yHtmlRenderer.d.ts +17 -0
- package/dist/A11yHtmlRenderer.js +102 -0
- package/dist/A11yReportAssets.d.ts +37 -0
- package/dist/A11yReportAssets.js +180 -0
- package/dist/A11yScanner.d.ts +25 -0
- package/dist/A11yScanner.js +124 -0
- package/dist/A11yTimeUtils.d.ts +9 -0
- package/dist/A11yTimeUtils.js +24 -0
- package/dist/SnapAllyLegacyReporter.d.ts +30 -0
- package/dist/SnapAllyLegacyReporter.js +235 -0
- package/dist/SnapAllyReporter.d.ts +44 -0
- package/dist/SnapAllyReporter.js +332 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +34 -0
- package/dist/models/index.d.ts +102 -0
- package/dist/models/index.js +18 -0
- package/dist/templates/accessibility-report.html +1412 -0
- package/dist/templates/execution-summary.html +695 -0
- package/dist/templates/test-execution-report.html +584 -0
- package/package.json +43 -0
|
@@ -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,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;
|