snap-ally 0.2.7-beta → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/A11yReportAssets.d.ts +5 -12
- package/dist/A11yReportAssets.js +16 -82
- package/dist/A11yScanner.d.ts +2 -21
- package/dist/A11yScanner.js +16 -22
- package/dist/A11yTimeUtils.js +11 -23
- package/dist/A11yVisualReporter.d.ts +50 -0
- package/dist/A11yVisualReporter.js +188 -0
- package/dist/AccessibilityReporterOptions.d.ts +24 -0
- package/dist/AccessibilityReporterOptions.js +5 -0
- package/dist/ResolvedColors.d.ts +15 -0
- package/dist/ResolvedColors.js +20 -0
- package/dist/SnapAllyReporter.d.ts +12 -72
- package/dist/SnapAllyReporter.js +218 -329
- package/dist/core/A11yHtmlRenderer.d.ts +17 -0
- package/dist/core/A11yHtmlRenderer.js +118 -0
- package/dist/core/A11yReportAssets.d.ts +30 -0
- package/dist/core/A11yReportAssets.js +127 -0
- package/dist/core/A11yScanner.d.ts +8 -0
- package/dist/core/A11yScanner.js +178 -0
- package/dist/core/A11yVisualReporter.d.ts +50 -0
- package/dist/core/A11yVisualReporter.js +188 -0
- package/dist/core/HtmlRenderer.d.ts +14 -0
- package/dist/core/HtmlRenderer.js +106 -0
- package/dist/core/ReportAssets.d.ts +29 -0
- package/dist/core/ReportAssets.js +126 -0
- package/dist/core/Scanner.d.ts +7 -0
- package/dist/core/Scanner.js +162 -0
- package/dist/core/VisualReporter.d.ts +54 -0
- package/dist/core/VisualReporter.js +192 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +13 -12
- package/dist/models/A11yDataSource.d.ts +15 -0
- package/dist/models/A11yDataSource.js +2 -0
- package/dist/models/A11yError.d.ts +34 -0
- package/dist/models/A11yError.js +11 -0
- package/dist/models/A11yScannerOptions.d.ts +24 -0
- package/dist/models/A11yScannerOptions.js +2 -0
- package/dist/models/AccessibilityReporterOptions.d.ts +24 -0
- package/dist/models/AccessibilityReporterOptions.js +5 -0
- package/dist/models/DataSource.d.ts +15 -0
- package/dist/models/DataSource.js +2 -0
- package/dist/models/ImagePath.d.ts +5 -0
- package/dist/models/ImagePath.js +3 -0
- package/dist/models/ReportData.d.ts +24 -0
- package/dist/models/ReportData.js +2 -0
- package/dist/models/ReporterOptions.d.ts +24 -0
- package/dist/models/ReporterOptions.js +5 -0
- package/dist/models/ResolvedColors.d.ts +16 -0
- package/dist/models/ResolvedColors.js +24 -0
- package/dist/models/ScannerOptions.d.ts +30 -0
- package/dist/models/ScannerOptions.js +2 -0
- package/dist/models/Severity.d.ts +7 -0
- package/dist/models/Severity.js +11 -0
- package/dist/models/Target.d.ts +10 -0
- package/dist/models/Target.js +3 -0
- package/dist/models/TestResults.d.ts +41 -0
- package/dist/models/TestResults.js +2 -0
- package/dist/models/TestStatusIcon.d.ts +8 -0
- package/dist/models/TestStatusIcon.js +12 -0
- package/dist/models/TestSummary.d.ts +34 -0
- package/dist/models/TestSummary.js +2 -0
- package/dist/models/Violation.d.ts +13 -0
- package/dist/models/Violation.js +2 -0
- package/dist/models/index.d.ts +12 -113
- package/dist/models/index.js +26 -16
- package/dist/templates/accessibility-report.html +62 -95
- package/dist/templates/execution-summary.html +37 -103
- package/dist/templates/global-report-styles.css +400 -9
- package/dist/templates/report-app.js +170 -72
- package/dist/templates/test-execution-report.html +84 -121
- package/dist/utils/A11yTimeUtils.d.ts +13 -0
- package/dist/utils/A11yTimeUtils.js +40 -0
- package/dist/utils/TimeUtils.d.ts +13 -0
- package/dist/utils/TimeUtils.js +39 -0
- package/package.json +2 -2
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.A11yVisualReporter = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Manages visual feedback on the page during an accessibility scan.
|
|
6
|
+
* Handles element highlights, violation banners, and report attachments.
|
|
7
|
+
*/
|
|
8
|
+
class A11yVisualReporter {
|
|
9
|
+
constructor(page) {
|
|
10
|
+
this.page = page;
|
|
11
|
+
this.rootId = 'snap-ally-visual-root';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Shows a violation banner at the bottom of the page.
|
|
15
|
+
*/
|
|
16
|
+
async showBanner(violation, color) {
|
|
17
|
+
await this.safeEvaluate(([v, rawColor, rootId, bannerId]) => {
|
|
18
|
+
const shadow = window.snapAllyGetRoot(rootId);
|
|
19
|
+
let container = shadow.getElementById(bannerId);
|
|
20
|
+
if (!container) {
|
|
21
|
+
const style = document.createElement('style');
|
|
22
|
+
style.textContent = `
|
|
23
|
+
#${bannerId} {
|
|
24
|
+
position: fixed;
|
|
25
|
+
left: 50%;
|
|
26
|
+
top: 24px;
|
|
27
|
+
transform: translateX(-50%);
|
|
28
|
+
width: calc(100% - 40px);
|
|
29
|
+
max-width: 600px;
|
|
30
|
+
padding: 12px 18px;
|
|
31
|
+
border-radius: 12px;
|
|
32
|
+
color: white;
|
|
33
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
34
|
+
font-size: 14px;
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 12px;
|
|
38
|
+
box-shadow: 0 12px 40px rgba(0,0,0,0.3);
|
|
39
|
+
backdrop-filter: blur(16px) saturate(180%);
|
|
40
|
+
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
|
41
|
+
border: 1px solid rgba(255,255,255,0.15);
|
|
42
|
+
z-index: 10000;
|
|
43
|
+
transition: all 0.3s ease;
|
|
44
|
+
}
|
|
45
|
+
.badge {
|
|
46
|
+
background: rgba(255,255,255,0.2);
|
|
47
|
+
padding: 2px 8px;
|
|
48
|
+
border-radius: 6px;
|
|
49
|
+
font-size: 11px;
|
|
50
|
+
font-weight: 700;
|
|
51
|
+
text-transform: uppercase;
|
|
52
|
+
border: 1px solid rgba(255,255,255,0.2);
|
|
53
|
+
}
|
|
54
|
+
.content { flex: 1; line-height: 1.4; font-weight: 500; }
|
|
55
|
+
`;
|
|
56
|
+
shadow.appendChild(style);
|
|
57
|
+
container = document.createElement('div');
|
|
58
|
+
container.id = bannerId;
|
|
59
|
+
shadow.appendChild(container);
|
|
60
|
+
}
|
|
61
|
+
container.style.backgroundColor = window.snapAllyToAlpha(rawColor, 0.85);
|
|
62
|
+
container.innerHTML = `
|
|
63
|
+
<div style="font-size: 20px;">⚠️</div>
|
|
64
|
+
<div class="content">
|
|
65
|
+
<div style="margin-bottom:4px; display:flex; align-items:center; gap:8px;">
|
|
66
|
+
<span class="badge">${v.id}</span>
|
|
67
|
+
<span style="opacity: 0.9;">${v.help}</span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
`;
|
|
71
|
+
}, [violation, color, this.rootId, A11yVisualReporter.BANNER_ID]);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Highlights an element on the page.
|
|
75
|
+
*/
|
|
76
|
+
async highlightElement(selector, color) {
|
|
77
|
+
await this.safeEvaluate(([sel, rawColor, rootId, highlightId]) => {
|
|
78
|
+
const target = document.querySelector(sel);
|
|
79
|
+
if (!target)
|
|
80
|
+
return;
|
|
81
|
+
target.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
82
|
+
const shadow = window.snapAllyGetRoot(rootId);
|
|
83
|
+
let highlight = shadow.getElementById(highlightId);
|
|
84
|
+
if (!highlight) {
|
|
85
|
+
const style = document.createElement('style');
|
|
86
|
+
style.textContent = `
|
|
87
|
+
#${highlightId} {
|
|
88
|
+
position: absolute;
|
|
89
|
+
pointer-events: none;
|
|
90
|
+
border-radius: 8px;
|
|
91
|
+
box-sizing: border-box;
|
|
92
|
+
z-index: 9999;
|
|
93
|
+
transition: all 0.2s ease;
|
|
94
|
+
box-shadow: 0 0 0 4px var(--c-alpha), 0 0 20px var(--c-alpha);
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
shadow.appendChild(style);
|
|
98
|
+
highlight = document.createElement('div');
|
|
99
|
+
highlight.id = highlightId;
|
|
100
|
+
shadow.appendChild(highlight);
|
|
101
|
+
}
|
|
102
|
+
const pad = 4;
|
|
103
|
+
const rect = target.getBoundingClientRect();
|
|
104
|
+
highlight.style.left = `${rect.left + window.scrollX - pad}px`;
|
|
105
|
+
highlight.style.top = `${rect.top + window.scrollY - pad}px`;
|
|
106
|
+
highlight.style.width = `${rect.width + pad * 2}px`;
|
|
107
|
+
highlight.style.height = `${rect.height + pad * 2}px`;
|
|
108
|
+
highlight.style.border = `3px solid ${rawColor}`;
|
|
109
|
+
highlight.style.setProperty('--c-alpha', window.snapAllyToAlpha(rawColor, 0.3));
|
|
110
|
+
}, [selector, color, this.rootId, A11yVisualReporter.HIGHLIGHT_ID]);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Removes all visual feedback from the page.
|
|
114
|
+
*/
|
|
115
|
+
async clean() {
|
|
116
|
+
await this.safeEvaluate((id) => { var _a; return (_a = document.getElementById(id)) === null || _a === void 0 ? void 0 : _a.remove(); }, this.rootId);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Removes the element highlight.
|
|
120
|
+
*/
|
|
121
|
+
async removeHighlight() {
|
|
122
|
+
await this.safeEvaluate(([rootId, hId]) => { var _a, _b, _c; return (_c = (_b = (_a = document.getElementById(rootId)) === null || _a === void 0 ? void 0 : _a.shadowRoot) === null || _b === void 0 ? void 0 : _b.getElementById(hId)) === null || _c === void 0 ? void 0 : _c.remove(); }, [this.rootId, A11yVisualReporter.HIGHLIGHT_ID]);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Attaches JSON data to the test report.
|
|
126
|
+
*/
|
|
127
|
+
async attachData(testInfo, name, data) {
|
|
128
|
+
await testInfo.attach(name, {
|
|
129
|
+
contentType: 'application/json',
|
|
130
|
+
body: Buffer.from(data),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Captures and attaches a screenshot to the test report.
|
|
135
|
+
*/
|
|
136
|
+
async captureScreenshot(name, testInfo) {
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
138
|
+
const { test } = require('@playwright/test');
|
|
139
|
+
return await test.step('Capture A11y screenshot', async () => {
|
|
140
|
+
const screenshot = await this.page.screenshot({ fullPage: false });
|
|
141
|
+
await testInfo.attach(name, { contentType: 'image/png', body: screenshot });
|
|
142
|
+
return screenshot;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Injects helper functions into the page context.
|
|
147
|
+
*/
|
|
148
|
+
async ensureHelpers() {
|
|
149
|
+
await this.page.evaluate(() => {
|
|
150
|
+
if (typeof window.snapAllyGetRoot === 'function')
|
|
151
|
+
return;
|
|
152
|
+
window.snapAllyGetRoot = (id) => {
|
|
153
|
+
let root = document.getElementById(id);
|
|
154
|
+
if (!root) {
|
|
155
|
+
root = document.createElement('div');
|
|
156
|
+
root.id = id;
|
|
157
|
+
root.style.cssText = 'position:absolute;top:0;left:0;width:0;height:0;z-index:2147483647;';
|
|
158
|
+
document.body.appendChild(root);
|
|
159
|
+
root.attachShadow({ mode: 'open' });
|
|
160
|
+
}
|
|
161
|
+
return root.shadowRoot;
|
|
162
|
+
};
|
|
163
|
+
window.snapAllyToAlpha = (color, alpha) => {
|
|
164
|
+
if (color.startsWith('rgba'))
|
|
165
|
+
return color.replace(/[\d.]+\)$/, `${alpha})`);
|
|
166
|
+
if (color.startsWith('rgb'))
|
|
167
|
+
return color.replace('rgb', 'rgba').replace(')', `, ${alpha})`);
|
|
168
|
+
const hex = Math.round(alpha * 255).toString(16).padStart(2, '0');
|
|
169
|
+
return `${color}${hex}`;
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async safeEvaluate(fn, arg) {
|
|
174
|
+
try {
|
|
175
|
+
await this.ensureHelpers();
|
|
176
|
+
await (arg !== undefined ? this.page.evaluate(fn, arg) : this.page.evaluate(fn));
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
180
|
+
if (msg.includes('closed') || msg.includes('destroyed') || msg.includes('ended'))
|
|
181
|
+
return;
|
|
182
|
+
throw e;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
exports.A11yVisualReporter = A11yVisualReporter;
|
|
187
|
+
A11yVisualReporter.BANNER_ID = 'snap-ally-banner';
|
|
188
|
+
A11yVisualReporter.HIGHLIGHT_ID = 'snap-ally-highlight';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles the rendering of HTML reports using static templates and JSON data injection.
|
|
3
|
+
*/
|
|
4
|
+
export declare class HtmlRenderer {
|
|
5
|
+
/**
|
|
6
|
+
* Renders a static HTML template by copying it and generating the accompanied data payload.
|
|
7
|
+
*/
|
|
8
|
+
render(templateName: string, data: Record<string, unknown>, _outputFolder: string, // Kept for signature compatibility
|
|
9
|
+
outputFileName: string): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Converts ANSI color codes to HTML spans for nicer error display.
|
|
12
|
+
*/
|
|
13
|
+
ansiToHtml(text: string): string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
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.HtmlRenderer = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
/**
|
|
40
|
+
* Handles the rendering of HTML reports using static templates and JSON data injection.
|
|
41
|
+
*/
|
|
42
|
+
class HtmlRenderer {
|
|
43
|
+
/**
|
|
44
|
+
* Renders a static HTML template by copying it and generating the accompanied data payload.
|
|
45
|
+
*/
|
|
46
|
+
async render(templateName, data, _outputFolder, // Kept for signature compatibility
|
|
47
|
+
outputFileName) {
|
|
48
|
+
// Resolve path relative to this file (dist/core/HtmlRenderer.js)
|
|
49
|
+
const templatesDir = path.join(__dirname, '..', 'templates');
|
|
50
|
+
const templatePath = path.join(templatesDir, templateName);
|
|
51
|
+
const cssPath = path.join(templatesDir, 'global-report-styles.css');
|
|
52
|
+
const jsPath = path.join(templatesDir, 'report-app.js');
|
|
53
|
+
if (!fs.existsSync(templatePath)) {
|
|
54
|
+
throw new Error(`[HtmlRenderer] Template not found: ${templatePath}`);
|
|
55
|
+
}
|
|
56
|
+
let html = fs.readFileSync(templatePath, 'utf8');
|
|
57
|
+
// Inline CSS
|
|
58
|
+
if (fs.existsSync(cssPath)) {
|
|
59
|
+
const css = fs.readFileSync(cssPath, 'utf8');
|
|
60
|
+
html = html.replace('</head>', `<style>\n${css}\n</style>\n</head>`);
|
|
61
|
+
// Remove the link tag if it exists
|
|
62
|
+
html = html.replace(/<link[^>]*global-report-styles\.css[^>]*>/, '');
|
|
63
|
+
}
|
|
64
|
+
// Inline Data
|
|
65
|
+
const jsData = `window.snapAllyData = ${JSON.stringify(data)};`;
|
|
66
|
+
html = html.replace('<script src="data.js"></script>', `<script>\n${jsData}\n</script>`);
|
|
67
|
+
// Inline main logic
|
|
68
|
+
if (fs.existsSync(jsPath)) {
|
|
69
|
+
const js = fs.readFileSync(jsPath, 'utf8');
|
|
70
|
+
html = html.replace('<script src="report-app.js"></script>', `<script>\n${js}\n</script>`);
|
|
71
|
+
}
|
|
72
|
+
// Final cleanup of any potential remaining dummy scripts
|
|
73
|
+
html = html.replace(/<script src="data-[^>]*\.js"><\/script>/, '');
|
|
74
|
+
// Ensure the output directory exists before writing
|
|
75
|
+
const outputDir = path.dirname(outputFileName);
|
|
76
|
+
if (!fs.existsSync(outputDir)) {
|
|
77
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
fs.writeFileSync(outputFileName, html, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Converts ANSI color codes to HTML spans for nicer error display.
|
|
83
|
+
*/
|
|
84
|
+
ansiToHtml(text) {
|
|
85
|
+
const map = {
|
|
86
|
+
'\u001b[30m': '<span style="color:black">',
|
|
87
|
+
'\u001b[31m': '<span style="color:red">',
|
|
88
|
+
'\u001b[32m': '<span style="color:green">',
|
|
89
|
+
'\u001b[33m': '<span style="color:yellow">',
|
|
90
|
+
'\u001b[34m': '<span style="color:blue">',
|
|
91
|
+
'\u001b[35m': '<span style="color:magenta">',
|
|
92
|
+
'\u001b[36m': '<span style="color:cyan">',
|
|
93
|
+
'\u001b[37m': '<span style="color:white">',
|
|
94
|
+
'\u001b[0m': '</span>',
|
|
95
|
+
'\u001b[2m': '<span style="opacity:0.5">',
|
|
96
|
+
'\u001b[22m': '</span>',
|
|
97
|
+
'\u001b[39m': '</span>',
|
|
98
|
+
};
|
|
99
|
+
let result = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
100
|
+
for (const [code, tag] of Object.entries(map)) {
|
|
101
|
+
result = result.split(code).join(tag);
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.HtmlRenderer = HtmlRenderer;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { TestResult } from '@playwright/test/reporter';
|
|
2
|
+
/**
|
|
3
|
+
* Utilities for managing and copying report assets like videos and screenshots.
|
|
4
|
+
*/
|
|
5
|
+
export declare class ReportAssets {
|
|
6
|
+
/**
|
|
7
|
+
* Copies a file from source to a destination folder.
|
|
8
|
+
*/
|
|
9
|
+
copyToFolder(destFolder: string, srcPath: string, fileName?: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Copies all video attachments to the report folder for portability.
|
|
12
|
+
*/
|
|
13
|
+
copyVideos(result: TestResult, destFolder: string): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Copies all screenshots found in the test attachments.
|
|
16
|
+
*/
|
|
17
|
+
copyScreenshots(result: TestResult, destFolder: string): string[];
|
|
18
|
+
/**
|
|
19
|
+
* Copies all other attachments (traces, logs, etc.) to the report folder.
|
|
20
|
+
*/
|
|
21
|
+
copyAllOtherAttachments(result: TestResult, destFolder: string): {
|
|
22
|
+
path: string;
|
|
23
|
+
name: string;
|
|
24
|
+
}[];
|
|
25
|
+
/**
|
|
26
|
+
* Persists an in-memory buffer to a file in the destination folder.
|
|
27
|
+
*/
|
|
28
|
+
saveBuffer(destFolder: string, fileName: string, buffer: Buffer): string;
|
|
29
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
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.ReportAssets = 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 ReportAssets {
|
|
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 all video attachments to the report folder for portability.
|
|
60
|
+
*/
|
|
61
|
+
copyVideos(result, destFolder) {
|
|
62
|
+
return result.attachments
|
|
63
|
+
.filter((a) => (a.name === 'video' || (a.contentType || '').startsWith('video/')) && a.path)
|
|
64
|
+
.map((attachment) => this.copyToFolder(destFolder, attachment.path))
|
|
65
|
+
.filter((p) => !!p);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Copies all screenshots found in the test attachments.
|
|
69
|
+
*/
|
|
70
|
+
copyScreenshots(result, destFolder) {
|
|
71
|
+
return result.attachments
|
|
72
|
+
.filter((a) => a.name === 'screenshot' ||
|
|
73
|
+
a.name.endsWith('.png') ||
|
|
74
|
+
(a.contentType || '').startsWith('image/'))
|
|
75
|
+
.map((a) => {
|
|
76
|
+
if (a.path) {
|
|
77
|
+
return this.copyToFolder(destFolder, a.path, a.name !== 'screenshot' ? a.name : undefined);
|
|
78
|
+
}
|
|
79
|
+
else if (a.body) {
|
|
80
|
+
const timestamp = Date.now();
|
|
81
|
+
const name = a.name === 'screenshot'
|
|
82
|
+
? `screenshot-${timestamp}.png`
|
|
83
|
+
: a.name.endsWith('.png')
|
|
84
|
+
? a.name
|
|
85
|
+
: `${a.name}.png`;
|
|
86
|
+
return this.saveBuffer(destFolder, name, a.body);
|
|
87
|
+
}
|
|
88
|
+
return '';
|
|
89
|
+
})
|
|
90
|
+
.filter((path) => path !== '');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Copies all other attachments (traces, logs, etc.) to the report folder.
|
|
94
|
+
*/
|
|
95
|
+
copyAllOtherAttachments(result, destFolder) {
|
|
96
|
+
const excludedNames = ['screenshot', 'video', 'A11y'];
|
|
97
|
+
return result.attachments
|
|
98
|
+
.filter((a) => !excludedNames.includes(a.name) &&
|
|
99
|
+
!a.name.toLowerCase().endsWith('.png') &&
|
|
100
|
+
!(a.contentType || '').startsWith('image/') &&
|
|
101
|
+
!(a.contentType || '').startsWith('video/'))
|
|
102
|
+
.map((a) => {
|
|
103
|
+
let name = '';
|
|
104
|
+
if (a.path) {
|
|
105
|
+
name = this.copyToFolder(destFolder, a.path, a.name);
|
|
106
|
+
}
|
|
107
|
+
else if (a.body) {
|
|
108
|
+
name = this.saveBuffer(destFolder, a.name, a.body);
|
|
109
|
+
}
|
|
110
|
+
return name ? { path: name, name: a.name } : null;
|
|
111
|
+
})
|
|
112
|
+
.filter((item) => item !== null);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Persists an in-memory buffer to a file in the destination folder.
|
|
116
|
+
*/
|
|
117
|
+
saveBuffer(destFolder, fileName, buffer) {
|
|
118
|
+
if (!fs.existsSync(destFolder)) {
|
|
119
|
+
fs.mkdirSync(destFolder, { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
const destFile = path.join(destFolder, fileName);
|
|
122
|
+
fs.writeFileSync(destFile, buffer);
|
|
123
|
+
return fileName;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.ReportAssets = ReportAssets;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Page, TestInfo } from '@playwright/test';
|
|
2
|
+
import { ScannerOptions } from '../models';
|
|
3
|
+
/**
|
|
4
|
+
* Performs an accessibility audit using Axe and Lighthouse.
|
|
5
|
+
*/
|
|
6
|
+
export declare function scanA11y(page: Page, testInfo: TestInfo, options?: ScannerOptions): Promise<void>;
|
|
7
|
+
export declare const checkAccessibility: typeof scanA11y;
|
|
@@ -0,0 +1,162 @@
|
|
|
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 VisualReporter_1 = require("./VisualReporter");
|
|
11
|
+
const models_1 = require("../models");
|
|
12
|
+
const TimeUtils_1 = require("../utils/TimeUtils");
|
|
13
|
+
/**
|
|
14
|
+
* Sanitizes a string to be safe for use in file paths and prevents path traversal attacks.
|
|
15
|
+
*/
|
|
16
|
+
function sanitizePageKey(input) {
|
|
17
|
+
return (input
|
|
18
|
+
.replace(/^https?:\/\//, '')
|
|
19
|
+
.replace(/[\/\\:*?"<>|]/g, '-')
|
|
20
|
+
.replace(/\.\./g, '')
|
|
21
|
+
.replace(/-+/g, '-')
|
|
22
|
+
.replace(/^-+|-+$/g, '')
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.substring(0, 200));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Performs an accessibility audit using Axe and Lighthouse.
|
|
28
|
+
*/
|
|
29
|
+
async function scanA11y(page, testInfo, options = {}) {
|
|
30
|
+
var _a, _b, _c, _d, _e, _f;
|
|
31
|
+
const showTerminal = (_a = options.verbose) !== null && _a !== void 0 ? _a : true;
|
|
32
|
+
const showBrowser = (_b = options.consoleLog) !== null && _b !== void 0 ? _b : true;
|
|
33
|
+
const rawPageKey = options.pageKey || page.url();
|
|
34
|
+
const pageKey = sanitizePageKey(rawPageKey);
|
|
35
|
+
const overlay = new VisualReporter_1.VisualReporter(page);
|
|
36
|
+
let axeBuilder = new playwright_1.default({ page });
|
|
37
|
+
const target = options.include || options.box;
|
|
38
|
+
if (target) {
|
|
39
|
+
if (typeof target === 'string') {
|
|
40
|
+
axeBuilder = axeBuilder.include(target);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
axeBuilder = axeBuilder.include(target);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (options.rules) {
|
|
47
|
+
axeBuilder = axeBuilder.options({ rules: options.rules });
|
|
48
|
+
}
|
|
49
|
+
if (options.tags) {
|
|
50
|
+
axeBuilder = axeBuilder.withTags(options.tags);
|
|
51
|
+
}
|
|
52
|
+
if (options.axeOptions) {
|
|
53
|
+
axeBuilder = axeBuilder.options(options.axeOptions);
|
|
54
|
+
}
|
|
55
|
+
let axeResults;
|
|
56
|
+
try {
|
|
57
|
+
axeResults = await axeBuilder.analyze();
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof Error &&
|
|
61
|
+
(error.message.includes('Test ended') ||
|
|
62
|
+
error.message.includes('Target page, context or browser has been closed'))) {
|
|
63
|
+
console.warn(`[SnapAlly] Accessibility scan skipped: ${error.message}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
const violationCount = axeResults.violations.length;
|
|
69
|
+
if ((showTerminal || showBrowser) && violationCount > 0) {
|
|
70
|
+
const mainMsg = `[A11yScanner] Violations found: ${violationCount}`;
|
|
71
|
+
const detailMessages = axeResults.violations.map(
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
+
(v, i) => ` ${i + 1}. ${v.id} [${v.impact}] - ${v.help}`);
|
|
74
|
+
if (showTerminal) {
|
|
75
|
+
console.log(`\n${mainMsg}`);
|
|
76
|
+
detailMessages.forEach((msg) => console.log(msg));
|
|
77
|
+
}
|
|
78
|
+
if (showBrowser) {
|
|
79
|
+
await page.evaluate(([mainMsg, details, color]) => {
|
|
80
|
+
console.log(`%c ${mainMsg}`, `color: ${color}; font-weight: bold; font-size: 12px;`);
|
|
81
|
+
details.forEach((msg) => console.log(msg));
|
|
82
|
+
}, [mainMsg, detailMessages, models_1.DEFAULT_COLORS.serious]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
test_1.expect
|
|
86
|
+
.soft(violationCount, `Accessibility audit failed with ${violationCount} violations.`)
|
|
87
|
+
.toBe(0);
|
|
88
|
+
const reporterConfig = testInfo.config.reporter.find((r) => Array.isArray(r) &&
|
|
89
|
+
(typeof r[0] === 'string' &&
|
|
90
|
+
(r[0].includes('SnapAllyReporter') || r[0].endsWith('src/SnapAllyReporter.ts'))));
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
+
const customColors = reporterConfig && Array.isArray(reporterConfig) ? (_c = reporterConfig[1]) === null || _c === void 0 ? void 0 : _c.colors : undefined;
|
|
93
|
+
const errors = [];
|
|
94
|
+
for (const violation of axeResults.violations) {
|
|
95
|
+
let errorIdx = 0;
|
|
96
|
+
const targets = [];
|
|
97
|
+
const severityColor = (0, models_1.getSeverityColor)(violation.impact, customColors);
|
|
98
|
+
for (const node of violation.nodes) {
|
|
99
|
+
for (const selector of node.target) {
|
|
100
|
+
const elementSelector = selector.toString();
|
|
101
|
+
const locator = page.locator(elementSelector);
|
|
102
|
+
await overlay.showBanner({ id: violation.id, help: violation.help }, severityColor);
|
|
103
|
+
if (await locator.isVisible()) {
|
|
104
|
+
await overlay.highlightElement(elementSelector, severityColor);
|
|
105
|
+
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
106
|
+
await page.waitForTimeout(100);
|
|
107
|
+
const screenshotName = `a11y-${violation.id}-${errorIdx++}.png`;
|
|
108
|
+
const buffer = await overlay.captureScreenshot(testInfo, screenshotName);
|
|
109
|
+
const excluded = new Set([
|
|
110
|
+
'Pre Condition',
|
|
111
|
+
'Post Condition',
|
|
112
|
+
'Description',
|
|
113
|
+
'A11y',
|
|
114
|
+
]);
|
|
115
|
+
const contextSteps = (testInfo.annotations || [])
|
|
116
|
+
.filter((a) => !excluded.has(a.type))
|
|
117
|
+
.map((a) => a.description || '');
|
|
118
|
+
const nodeHtml = node.html || '';
|
|
119
|
+
const friendlySnippet = elementSelector;
|
|
120
|
+
targets.push({
|
|
121
|
+
element: elementSelector,
|
|
122
|
+
snippet: friendlySnippet,
|
|
123
|
+
html: nodeHtml,
|
|
124
|
+
screenshot: screenshotName,
|
|
125
|
+
steps: contextSteps,
|
|
126
|
+
stepsJson: JSON.stringify(contextSteps),
|
|
127
|
+
screenshotBase64: buffer.toString('base64'),
|
|
128
|
+
});
|
|
129
|
+
await overlay.removeHighlight();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
errors.push({
|
|
134
|
+
id: violation.id,
|
|
135
|
+
description: violation.description,
|
|
136
|
+
severity: violation.impact || 'unknown',
|
|
137
|
+
helpUrl: violation.helpUrl,
|
|
138
|
+
help: violation.help,
|
|
139
|
+
guideline: violation.tags[1] || 'N/A',
|
|
140
|
+
wcagRule: violation.tags.find((t) => t.startsWith('wcag')) || violation.tags[1] || 'N/A',
|
|
141
|
+
total: targets.length || violation.nodes.length,
|
|
142
|
+
target: targets,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const reportData = {
|
|
146
|
+
pageKey,
|
|
147
|
+
pageUrl: page.url(),
|
|
148
|
+
accessibilityScore: 0,
|
|
149
|
+
a11yErrors: errors,
|
|
150
|
+
criticalColor: (customColors === null || customColors === void 0 ? void 0 : customColors.critical) || models_1.DEFAULT_COLORS.critical,
|
|
151
|
+
seriousColor: (customColors === null || customColors === void 0 ? void 0 : customColors.serious) || models_1.DEFAULT_COLORS.serious,
|
|
152
|
+
moderateColor: (customColors === null || customColors === void 0 ? void 0 : customColors.moderate) || models_1.DEFAULT_COLORS.moderate,
|
|
153
|
+
minorColor: (customColors === null || customColors === void 0 ? void 0 : customColors.minor) || models_1.DEFAULT_COLORS.minor,
|
|
154
|
+
adoOrganization: ((_d = options.ado) === null || _d === void 0 ? void 0 : _d.organization) || process.env.ADO_ORGANIZATION || '',
|
|
155
|
+
adoProject: ((_e = options.ado) === null || _e === void 0 ? void 0 : _e.project) || process.env.ADO_PROJECT || '',
|
|
156
|
+
adoAreaPath: ((_f = options.ado) === null || _f === void 0 ? void 0 : _f.areaPath) || process.env.ADO_AREA_PATH || '',
|
|
157
|
+
timestamp: TimeUtils_1.TimeUtils.formatDate(new Date()),
|
|
158
|
+
};
|
|
159
|
+
await overlay.attachJsonData(testInfo, 'A11y', JSON.stringify(reportData));
|
|
160
|
+
await overlay.cleanupOverlay();
|
|
161
|
+
}
|
|
162
|
+
exports.checkAccessibility = scanA11y;
|