snap-ally 0.1.1-beta → 0.2.2-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/A11yAuditOverlay.d.ts +47 -18
- package/dist/A11yAuditOverlay.js +211 -107
- package/dist/A11yHtmlRenderer.js +10 -4
- package/dist/A11yScanner.d.ts +1 -1
- package/dist/A11yScanner.js +5 -3
- package/dist/SnapAllyReporter.d.ts +56 -10
- package/dist/SnapAllyReporter.js +305 -240
- package/dist/templates/report-app.js +50 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,19 @@ A powerful, developer-friendly Playwright reporter for **Accessibility testing**
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## 🤔 Why the name "Snap-Ally"?
|
|
11
|
+
|
|
12
|
+
- **Snap**: Like a snapshot, it provides an instant picture of a website's accessibility state at the moment the tests are executed.
|
|
13
|
+
- **Ally**: It resembles **a11y** (the abbreviation for accessibility) and serves as an ally that allows you to create bugs in Azure DevOps more easily.
|
|
14
|
+
|
|
15
|
+
## 💡 Motivation
|
|
16
|
+
|
|
17
|
+
I have seen closely how much people with disabilities struggle with something as fundamental as finding a job.
|
|
18
|
+
|
|
19
|
+
I believe that with relatively simple changes in HTML, good color contrast, among other things, systems should work and help all people equally. Since about 15% of the world's population lives with some form of disability.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
10
23
|
## 📺 Demo
|
|
11
24
|
|
|
12
25
|
**[▶️ Watch the Demo Video](https://www.loom.com/share/853c04f1f76242a699e8f82e54733007)**
|
|
@@ -1,42 +1,71 @@
|
|
|
1
|
-
import { Page, TestInfo } from '@playwright/test';
|
|
2
|
-
export interface AuditAnnotation {
|
|
3
|
-
type: string;
|
|
4
|
-
description: string;
|
|
5
|
-
keyPage: string;
|
|
6
|
-
}
|
|
1
|
+
import type { Page, TestInfo } from '@playwright/test';
|
|
7
2
|
/**
|
|
8
3
|
* Handles visual feedback and Playwright annotations during an accessibility audit.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* - Rendering violation banners and element highlights via a Shadow DOM overlay
|
|
7
|
+
* - Capturing screenshots and attaching them to the Playwright test report
|
|
8
|
+
*
|
|
9
|
+
* All DOM mutations are isolated inside a Shadow DOM root to avoid
|
|
10
|
+
* interfering with the page under test.
|
|
9
11
|
*/
|
|
10
12
|
export declare class A11yAuditOverlay {
|
|
11
|
-
|
|
12
|
-
protected keyPage: string;
|
|
13
|
+
private readonly page;
|
|
13
14
|
private readonly overlayRootId;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/** IDs for elements created inside the shadow root. */
|
|
16
|
+
private static readonly BANNER_ID;
|
|
17
|
+
private static readonly HIGHLIGHT_ID;
|
|
18
|
+
constructor(page: Page);
|
|
17
19
|
/**
|
|
18
20
|
* Shows a compact, modern banner at the bottom of the page describing the violation.
|
|
21
|
+
*
|
|
22
|
+
* @param violation - Object containing the Axe rule `id` and human-readable `help` text.
|
|
23
|
+
* @param color - CSS colour used as the banner background (rgb/rgba/hex).
|
|
19
24
|
*/
|
|
20
25
|
showViolationOverlay(violation: {
|
|
21
26
|
id: string;
|
|
22
27
|
help: string;
|
|
23
28
|
}, color: string): Promise<void>;
|
|
24
29
|
/**
|
|
25
|
-
* Removes the violation description
|
|
30
|
+
* Removes the violation description banner from the page.
|
|
26
31
|
*/
|
|
27
32
|
hideViolationOverlay(): Promise<void>;
|
|
28
33
|
/**
|
|
29
|
-
*
|
|
34
|
+
* Draws a glowing highlight border around the element matching `selector`.
|
|
35
|
+
*
|
|
36
|
+
* The element is scrolled into view first so that the highlight coordinates
|
|
37
|
+
* are accurate after any layout shift.
|
|
38
|
+
*
|
|
39
|
+
* @param selector - A CSS selector that uniquely identifies the target element.
|
|
40
|
+
* @param color - CSS colour for the highlight border and glow.
|
|
41
|
+
*/
|
|
42
|
+
highlightElement(selector: string, color: string): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Removes the element highlight from the page.
|
|
45
|
+
*/
|
|
46
|
+
unhighlightElement(): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Attaches arbitrary data to the Playwright test report.
|
|
49
|
+
*
|
|
50
|
+
* @param testInfo - The current Playwright `TestInfo` instance.
|
|
51
|
+
* @param name - Attachment name shown in the report.
|
|
52
|
+
* @param description - Content to attach (serialised as JSON by the caller).
|
|
30
53
|
*/
|
|
31
54
|
addTestAttachment(testInfo: TestInfo, name: string, description: string): Promise<void>;
|
|
32
|
-
getAuditAnnotations(): AuditAnnotation[];
|
|
33
55
|
/**
|
|
34
|
-
* Captures a screenshot and attaches it to the test report.
|
|
56
|
+
* Captures a viewport screenshot and attaches it to the test report.
|
|
57
|
+
*
|
|
58
|
+
* The screenshot uses `fullPage: false` (viewport only) to avoid browser
|
|
59
|
+
* resizing flashes that can occur with full-page captures.
|
|
60
|
+
*
|
|
61
|
+
* @param fileName - Name for the attachment in the test report.
|
|
62
|
+
* @param testInfo - The current Playwright `TestInfo` instance.
|
|
63
|
+
* @returns The raw screenshot buffer.
|
|
35
64
|
*/
|
|
36
65
|
captureAndAttachScreenshot(fileName: string, testInfo: TestInfo): Promise<Buffer>;
|
|
37
|
-
highlightElement(selector: string, color: string): Promise<void>;
|
|
38
66
|
/**
|
|
39
|
-
*
|
|
67
|
+
* Wrapper around `page.evaluate` that silently swallows errors caused by
|
|
68
|
+
* the page or browser closing mid-evaluation (e.g. navigation, test teardown).
|
|
40
69
|
*/
|
|
41
|
-
|
|
70
|
+
private safeEvaluate;
|
|
42
71
|
}
|
package/dist/A11yAuditOverlay.js
CHANGED
|
@@ -1,40 +1,60 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.A11yAuditOverlay = void 0;
|
|
4
|
-
const test_1 = require("@playwright/test");
|
|
5
4
|
/**
|
|
6
5
|
* Handles visual feedback and Playwright annotations during an accessibility audit.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* - Rendering violation banners and element highlights via a Shadow DOM overlay
|
|
9
|
+
* - Capturing screenshots and attaching them to the Playwright test report
|
|
10
|
+
*
|
|
11
|
+
* All DOM mutations are isolated inside a Shadow DOM root to avoid
|
|
12
|
+
* interfering with the page under test.
|
|
7
13
|
*/
|
|
8
14
|
class A11yAuditOverlay {
|
|
9
|
-
constructor(page
|
|
15
|
+
constructor(page) {
|
|
10
16
|
this.page = page;
|
|
11
|
-
this.keyPage = keyPage;
|
|
12
17
|
this.overlayRootId = 'a11y-audit-overlay-root';
|
|
13
|
-
this.auditAnnotations = [];
|
|
14
|
-
}
|
|
15
|
-
reset() {
|
|
16
|
-
this.auditAnnotations = [];
|
|
17
18
|
}
|
|
19
|
+
// ──────────────────────────────────────────────
|
|
20
|
+
// Violation banner
|
|
21
|
+
// ──────────────────────────────────────────────
|
|
18
22
|
/**
|
|
19
23
|
* Shows a compact, modern banner at the bottom of the page describing the violation.
|
|
24
|
+
*
|
|
25
|
+
* @param violation - Object containing the Axe rule `id` and human-readable `help` text.
|
|
26
|
+
* @param color - CSS colour used as the banner background (rgb/rgba/hex).
|
|
20
27
|
*/
|
|
21
28
|
async showViolationOverlay(violation, color) {
|
|
22
|
-
await this.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
'
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
const
|
|
33
|
-
|
|
29
|
+
await this.safeEvaluate(([v, rawColor, rootId, bannerId]) => {
|
|
30
|
+
// --- helpers scoped to the browser context ---
|
|
31
|
+
const toAlphaColor = (c, alpha = 0.85) => {
|
|
32
|
+
if (c.includes('rgba'))
|
|
33
|
+
return c;
|
|
34
|
+
if (c.includes('rgb'))
|
|
35
|
+
return c.replace('rgb', 'rgba').replace(')', `, ${alpha})`);
|
|
36
|
+
// Hex – append alpha byte (0xD9 ≈ 0.85)
|
|
37
|
+
return `${c}D9`;
|
|
38
|
+
};
|
|
39
|
+
const getOrCreateRoot = (id) => {
|
|
40
|
+
let root = document.getElementById(id);
|
|
41
|
+
if (!root) {
|
|
42
|
+
root = document.createElement('div');
|
|
43
|
+
root.id = id;
|
|
44
|
+
root.style.cssText =
|
|
45
|
+
'position:absolute;top:0;left:0;width:0;height:0;z-index:2147483647;';
|
|
46
|
+
document.body.appendChild(root);
|
|
47
|
+
root.attachShadow({ mode: 'open' });
|
|
48
|
+
}
|
|
49
|
+
return root.shadowRoot;
|
|
50
|
+
};
|
|
51
|
+
// --- main logic ---
|
|
52
|
+
const shadow = getOrCreateRoot(rootId);
|
|
53
|
+
let container = shadow.getElementById(bannerId);
|
|
34
54
|
if (!container) {
|
|
35
55
|
const style = document.createElement('style');
|
|
36
56
|
style.textContent = `
|
|
37
|
-
|
|
57
|
+
#${bannerId} {
|
|
38
58
|
position: fixed;
|
|
39
59
|
left: 50%;
|
|
40
60
|
bottom: 24px;
|
|
@@ -56,7 +76,7 @@ class A11yAuditOverlay {
|
|
|
56
76
|
z-index: 10000;
|
|
57
77
|
}
|
|
58
78
|
.badge {
|
|
59
|
-
background: rgba(255,
|
|
79
|
+
background: rgba(255,255,255,0.2);
|
|
60
80
|
padding: 2px 8px;
|
|
61
81
|
border-radius: 6px;
|
|
62
82
|
font-size: 11px;
|
|
@@ -74,126 +94,210 @@ class A11yAuditOverlay {
|
|
|
74
94
|
`;
|
|
75
95
|
shadow.appendChild(style);
|
|
76
96
|
container = document.createElement('div');
|
|
77
|
-
container.id =
|
|
97
|
+
container.id = bannerId;
|
|
78
98
|
shadow.appendChild(container);
|
|
79
99
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
container.style.backgroundColor = toAlphaColor(rawColor);
|
|
101
|
+
// Build DOM nodes instead of innerHTML to prevent XSS
|
|
102
|
+
container.textContent = ''; // clear previous content
|
|
103
|
+
const icon = document.createElement('div');
|
|
104
|
+
icon.style.fontSize = '20px';
|
|
105
|
+
icon.textContent = '⚠️';
|
|
106
|
+
const badge = document.createElement('span');
|
|
107
|
+
badge.className = 'badge';
|
|
108
|
+
badge.textContent = v.id;
|
|
109
|
+
const helpText = document.createElement('span');
|
|
110
|
+
helpText.style.opacity = '0.9';
|
|
111
|
+
helpText.textContent = v.help;
|
|
112
|
+
const row = document.createElement('div');
|
|
113
|
+
row.style.cssText = 'margin-bottom:4px;display:flex;align-items:center;gap:8px;';
|
|
114
|
+
row.appendChild(badge);
|
|
115
|
+
row.appendChild(helpText);
|
|
116
|
+
const content = document.createElement('div');
|
|
117
|
+
content.className = 'content';
|
|
118
|
+
content.appendChild(row);
|
|
119
|
+
container.appendChild(icon);
|
|
120
|
+
container.appendChild(content);
|
|
121
|
+
}, [
|
|
122
|
+
violation,
|
|
123
|
+
color,
|
|
124
|
+
this.overlayRootId,
|
|
125
|
+
A11yAuditOverlay.BANNER_ID,
|
|
126
|
+
]);
|
|
96
127
|
}
|
|
97
128
|
/**
|
|
98
|
-
* Removes the violation description
|
|
129
|
+
* Removes the violation description banner from the page.
|
|
99
130
|
*/
|
|
100
131
|
async hideViolationOverlay() {
|
|
101
|
-
await this.
|
|
132
|
+
await this.safeEvaluate((rootId) => {
|
|
102
133
|
const el = document.getElementById(rootId);
|
|
103
134
|
if (el)
|
|
104
135
|
el.remove();
|
|
105
136
|
}, this.overlayRootId);
|
|
106
137
|
}
|
|
138
|
+
// ──────────────────────────────────────────────
|
|
139
|
+
// Element highlighting
|
|
140
|
+
// ──────────────────────────────────────────────
|
|
107
141
|
/**
|
|
108
|
-
*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
getAuditAnnotations() {
|
|
117
|
-
return this.auditAnnotations;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Captures a screenshot and attaches it to the test report.
|
|
142
|
+
* Draws a glowing highlight border around the element matching `selector`.
|
|
143
|
+
*
|
|
144
|
+
* The element is scrolled into view first so that the highlight coordinates
|
|
145
|
+
* are accurate after any layout shift.
|
|
146
|
+
*
|
|
147
|
+
* @param selector - A CSS selector that uniquely identifies the target element.
|
|
148
|
+
* @param color - CSS colour for the highlight border and glow.
|
|
121
149
|
*/
|
|
122
|
-
async captureAndAttachScreenshot(fileName, testInfo) {
|
|
123
|
-
return await test_1.test.step('Capture A11y screenshot', async () => {
|
|
124
|
-
// Use viewport screenshot instead of fullPage to avoid browser resizing flashes
|
|
125
|
-
const screenshot = await this.page.screenshot({ fullPage: false });
|
|
126
|
-
await testInfo.attach(fileName, { contentType: 'image/png', body: screenshot });
|
|
127
|
-
return screenshot;
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
150
|
async highlightElement(selector, color) {
|
|
131
|
-
await this.
|
|
151
|
+
await this.safeEvaluate(([sel, rawColor, rootId, highlightId]) => {
|
|
132
152
|
const target = document.querySelector(sel);
|
|
133
153
|
if (!target)
|
|
134
154
|
return;
|
|
135
|
-
// Scroll FIRST to ensure accurate coordinates after scroll
|
|
136
155
|
target.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
const toAlphaColor = (c, alpha) => {
|
|
157
|
+
if (c.includes('rgba'))
|
|
158
|
+
return c.replace(/[\d.]+\)$/, `${alpha})`);
|
|
159
|
+
if (c.includes('rgb'))
|
|
160
|
+
return c.replace('rgb', 'rgba').replace(')', `, ${alpha})`);
|
|
161
|
+
// Hex – convert alpha to 2-char hex
|
|
162
|
+
const hex = Math.round(alpha * 255)
|
|
163
|
+
.toString(16)
|
|
164
|
+
.padStart(2, '0');
|
|
165
|
+
return `${c}${hex}`;
|
|
166
|
+
};
|
|
167
|
+
const getOrCreateRoot = (id) => {
|
|
168
|
+
let root = document.getElementById(id);
|
|
169
|
+
if (!root) {
|
|
170
|
+
root = document.createElement('div');
|
|
171
|
+
root.id = id;
|
|
172
|
+
root.style.cssText =
|
|
173
|
+
'position:absolute;top:0;left:0;width:0;height:0;z-index:2147483647;';
|
|
174
|
+
document.body.appendChild(root);
|
|
175
|
+
root.attachShadow({ mode: 'open' });
|
|
176
|
+
}
|
|
177
|
+
return root.shadowRoot;
|
|
178
|
+
};
|
|
179
|
+
const shadow = getOrCreateRoot(rootId);
|
|
180
|
+
let highlight = shadow.getElementById(highlightId);
|
|
148
181
|
if (!highlight) {
|
|
149
182
|
const style = document.createElement('style');
|
|
150
183
|
style.textContent = `
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
border: 2px solid var(--c);
|
|
167
|
-
}
|
|
168
|
-
`;
|
|
184
|
+
#${highlightId} {
|
|
185
|
+
position: absolute;
|
|
186
|
+
pointer-events: none;
|
|
187
|
+
border-radius: 8px;
|
|
188
|
+
box-sizing: border-box;
|
|
189
|
+
z-index: 9999;
|
|
190
|
+
box-shadow: 0 0 0 4px var(--c-alpha), 0 0 20px var(--c-alpha);
|
|
191
|
+
}
|
|
192
|
+
.glow {
|
|
193
|
+
position: absolute;
|
|
194
|
+
inset: 0;
|
|
195
|
+
border-radius: inherit;
|
|
196
|
+
border: 2px solid var(--c);
|
|
197
|
+
}
|
|
198
|
+
`;
|
|
169
199
|
shadow.appendChild(style);
|
|
170
200
|
highlight = document.createElement('div');
|
|
171
|
-
highlight.id =
|
|
172
|
-
|
|
201
|
+
highlight.id = highlightId;
|
|
202
|
+
const glow = document.createElement('div');
|
|
203
|
+
glow.className = 'glow';
|
|
204
|
+
highlight.appendChild(glow);
|
|
173
205
|
shadow.appendChild(highlight);
|
|
174
206
|
}
|
|
207
|
+
const pad = 4;
|
|
175
208
|
const rect = target.getBoundingClientRect();
|
|
176
|
-
highlight.style.left = `${rect.left + window.scrollX -
|
|
177
|
-
highlight.style.top = `${rect.top + window.scrollY -
|
|
178
|
-
highlight.style.width = `${rect.width +
|
|
179
|
-
highlight.style.height = `${rect.height +
|
|
180
|
-
highlight.style.border = `3px solid ${
|
|
181
|
-
highlight.style.setProperty('--c',
|
|
182
|
-
highlight.style.setProperty('--c-alpha',
|
|
183
|
-
}, [
|
|
209
|
+
highlight.style.left = `${rect.left + window.scrollX - pad}px`;
|
|
210
|
+
highlight.style.top = `${rect.top + window.scrollY - pad}px`;
|
|
211
|
+
highlight.style.width = `${rect.width + pad * 2}px`;
|
|
212
|
+
highlight.style.height = `${rect.height + pad * 2}px`;
|
|
213
|
+
highlight.style.border = `3px solid ${rawColor}`;
|
|
214
|
+
highlight.style.setProperty('--c', rawColor);
|
|
215
|
+
highlight.style.setProperty('--c-alpha', toAlphaColor(rawColor, 0.3));
|
|
216
|
+
}, [
|
|
217
|
+
selector,
|
|
218
|
+
color,
|
|
219
|
+
this.overlayRootId,
|
|
220
|
+
A11yAuditOverlay.HIGHLIGHT_ID,
|
|
221
|
+
]);
|
|
184
222
|
}
|
|
185
223
|
/**
|
|
186
|
-
* Removes
|
|
224
|
+
* Removes the element highlight from the page.
|
|
187
225
|
*/
|
|
188
226
|
async unhighlightElement() {
|
|
189
|
-
await this.
|
|
227
|
+
await this.safeEvaluate(([rootId, highlightId]) => {
|
|
190
228
|
const root = document.getElementById(rootId);
|
|
191
|
-
if (root
|
|
192
|
-
const highlight = root.shadowRoot.getElementById(
|
|
229
|
+
if (root === null || root === void 0 ? void 0 : root.shadowRoot) {
|
|
230
|
+
const highlight = root.shadowRoot.getElementById(highlightId);
|
|
193
231
|
if (highlight)
|
|
194
232
|
highlight.remove();
|
|
195
233
|
}
|
|
196
|
-
}, this.overlayRootId);
|
|
234
|
+
}, [this.overlayRootId, A11yAuditOverlay.HIGHLIGHT_ID]);
|
|
235
|
+
}
|
|
236
|
+
// ──────────────────────────────────────────────
|
|
237
|
+
// Test report helpers
|
|
238
|
+
// ──────────────────────────────────────────────
|
|
239
|
+
/**
|
|
240
|
+
* Attaches arbitrary data to the Playwright test report.
|
|
241
|
+
*
|
|
242
|
+
* @param testInfo - The current Playwright `TestInfo` instance.
|
|
243
|
+
* @param name - Attachment name shown in the report.
|
|
244
|
+
* @param description - Content to attach (serialised as JSON by the caller).
|
|
245
|
+
*/
|
|
246
|
+
async addTestAttachment(testInfo, name, description) {
|
|
247
|
+
await testInfo.attach(name, {
|
|
248
|
+
contentType: 'application/json',
|
|
249
|
+
body: Buffer.from(description),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Captures a viewport screenshot and attaches it to the test report.
|
|
254
|
+
*
|
|
255
|
+
* The screenshot uses `fullPage: false` (viewport only) to avoid browser
|
|
256
|
+
* resizing flashes that can occur with full-page captures.
|
|
257
|
+
*
|
|
258
|
+
* @param fileName - Name for the attachment in the test report.
|
|
259
|
+
* @param testInfo - The current Playwright `TestInfo` instance.
|
|
260
|
+
* @returns The raw screenshot buffer.
|
|
261
|
+
*/
|
|
262
|
+
async captureAndAttachScreenshot(fileName, testInfo) {
|
|
263
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
264
|
+
const { test } = require('@playwright/test');
|
|
265
|
+
return await test.step('Capture A11y screenshot', async () => {
|
|
266
|
+
const screenshot = await this.page.screenshot({ fullPage: false });
|
|
267
|
+
await testInfo.attach(fileName, { contentType: 'image/png', body: screenshot });
|
|
268
|
+
return screenshot;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
// ──────────────────────────────────────────────
|
|
272
|
+
// Private helpers
|
|
273
|
+
// ──────────────────────────────────────────────
|
|
274
|
+
/**
|
|
275
|
+
* Wrapper around `page.evaluate` that silently swallows errors caused by
|
|
276
|
+
* the page or browser closing mid-evaluation (e.g. navigation, test teardown).
|
|
277
|
+
*/
|
|
278
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
279
|
+
async safeEvaluate(fn, arg) {
|
|
280
|
+
try {
|
|
281
|
+
if (arg !== undefined) {
|
|
282
|
+
await this.page.evaluate(fn, arg);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
await this.page.evaluate(fn);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
if (error instanceof Error &&
|
|
290
|
+
(error.message.includes('Target page, context or browser has been closed') ||
|
|
291
|
+
error.message.includes('Execution context was destroyed') ||
|
|
292
|
+
error.message.includes('Test ended'))) {
|
|
293
|
+
// Page navigated away or test ended — overlay is no longer relevant.
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
197
298
|
}
|
|
198
299
|
}
|
|
199
300
|
exports.A11yAuditOverlay = A11yAuditOverlay;
|
|
301
|
+
/** IDs for elements created inside the shadow root. */
|
|
302
|
+
A11yAuditOverlay.BANNER_ID = 'a11y-banner';
|
|
303
|
+
A11yAuditOverlay.HIGHLIGHT_ID = 'a11y-highlight';
|
package/dist/A11yHtmlRenderer.js
CHANGED
|
@@ -59,11 +59,17 @@ class A11yHtmlRenderer {
|
|
|
59
59
|
if (!fs.existsSync(outputFolder)) {
|
|
60
60
|
fs.mkdirSync(outputFolder, { recursive: true });
|
|
61
61
|
}
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// Derive a unique data filename from the HTML filename to avoid collisions
|
|
63
|
+
// when multiple reports (e.g. accessibility + execution) share the same folder.
|
|
64
|
+
const htmlBaseName = path.basename(outputFileName, '.html');
|
|
65
|
+
const dataJsName = `data-${htmlBaseName}.js`;
|
|
66
|
+
// 1. Copy the HTML template and patch the data.js reference to the unique name
|
|
67
|
+
let templateHtml = fs.readFileSync(templatePath, 'utf8');
|
|
68
|
+
templateHtml = templateHtml.replace(/(<script\s+src=")data\.js(")/, `$1${dataJsName}$2`);
|
|
69
|
+
fs.writeFileSync(outputFileName, templateHtml, 'utf8');
|
|
70
|
+
// 2. Wrap the report data in a JS variable and write the per-report data file
|
|
65
71
|
const outputDir = path.dirname(outputFileName);
|
|
66
|
-
const dataJsPath = path.join(outputDir,
|
|
72
|
+
const dataJsPath = path.join(outputDir, dataJsName);
|
|
67
73
|
const jsContent = `window.snapAllyData = ${JSON.stringify(data)};`;
|
|
68
74
|
fs.writeFileSync(dataJsPath, jsContent, 'utf8');
|
|
69
75
|
// 3. Copy the global CSS and JS rendering engine next to the HTML file
|
package/dist/A11yScanner.d.ts
CHANGED
package/dist/A11yScanner.js
CHANGED
|
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.checkAccessibility = void 0;
|
|
7
7
|
exports.scanA11y = scanA11y;
|
|
8
8
|
const playwright_1 = __importDefault(require("@axe-core/playwright"));
|
|
9
|
-
const test_1 = require("@playwright/test");
|
|
10
9
|
const A11yAuditOverlay_1 = require("./A11yAuditOverlay");
|
|
11
10
|
const models_1 = require("./models");
|
|
12
11
|
const A11yTimeUtils_1 = require("./A11yTimeUtils");
|
|
@@ -41,7 +40,7 @@ async function scanA11y(page, testInfo, options = {}) {
|
|
|
41
40
|
// Sanitize pageKey to prevent path traversal attacks
|
|
42
41
|
const rawPageKey = options.pageKey || page.url();
|
|
43
42
|
const pageKey = sanitizePageKey(rawPageKey);
|
|
44
|
-
const overlay = new A11yAuditOverlay_1.A11yAuditOverlay(page
|
|
43
|
+
const overlay = new A11yAuditOverlay_1.A11yAuditOverlay(page);
|
|
45
44
|
// Configure Axe
|
|
46
45
|
let axeBuilder = new playwright_1.default({ page });
|
|
47
46
|
const target = options.include || options.box;
|
|
@@ -96,7 +95,10 @@ async function scanA11y(page, testInfo, options = {}) {
|
|
|
96
95
|
}
|
|
97
96
|
}
|
|
98
97
|
// Fail the test if violations found (softly)
|
|
99
|
-
|
|
98
|
+
// Dynamically require to avoid eager loading @playwright/test during config evaluation
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
100
|
+
const { expect } = require('@playwright/test');
|
|
101
|
+
expect
|
|
100
102
|
.soft(violationCount, `Accessibility audit failed with ${violationCount} violations.`)
|
|
101
103
|
.toBe(0);
|
|
102
104
|
// Run Axe Audit
|
|
@@ -24,22 +24,68 @@ export interface AccessibilityReporterOptions {
|
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Playwright reporter for accessibility audits and test steps.
|
|
27
|
-
*
|
|
27
|
+
*
|
|
28
|
+
* Generates:
|
|
29
|
+
* - A per-test execution report (steps, video, screenshots, errors)
|
|
30
|
+
* - A per-scan accessibility report (violations, evidence, ADO integration)
|
|
31
|
+
* - A global execution summary with per-browser breakdowns
|
|
28
32
|
*/
|
|
29
33
|
declare class SnapAllyReporter implements Reporter {
|
|
30
|
-
private
|
|
31
|
-
private
|
|
32
|
-
private
|
|
33
|
-
private
|
|
34
|
-
private
|
|
34
|
+
private readonly outputFolder;
|
|
35
|
+
private readonly assetsManager;
|
|
36
|
+
private readonly renderer;
|
|
37
|
+
private readonly options;
|
|
38
|
+
private readonly colors;
|
|
35
39
|
private projectRoot;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Monotonically increasing test counter.
|
|
42
|
+
* Incremented synchronously in {@link onTestEnd} to avoid race conditions
|
|
43
|
+
* when multiple async {@link processTestResult} calls run concurrently.
|
|
44
|
+
*/
|
|
45
|
+
private testIndex;
|
|
46
|
+
/** Async tasks queued by `onTestEnd`; drained in `onEnd`. */
|
|
47
|
+
private readonly tasks;
|
|
48
|
+
/** Aggregated data for the final summary report. */
|
|
49
|
+
private readonly executionSummary;
|
|
39
50
|
constructor(options?: AccessibilityReporterOptions);
|
|
51
|
+
printsToStdio(): boolean;
|
|
40
52
|
onBegin(config: FullConfig): void;
|
|
41
53
|
onTestEnd(test: TestCase, result: TestResult): void;
|
|
42
|
-
private processTestResult;
|
|
43
54
|
onEnd(result: FullResult): Promise<void>;
|
|
55
|
+
private processTestResult;
|
|
56
|
+
/** Extracts structured metadata from test annotations and result steps. */
|
|
57
|
+
private extractTestMetadata;
|
|
58
|
+
/** Determines the browser name for the current test. */
|
|
59
|
+
private resolveBrowser;
|
|
60
|
+
/** Converts Playwright error objects into HTML-safe strings. */
|
|
61
|
+
private extractErrorLogs;
|
|
62
|
+
/** Return value for {@link processAccessibilityData}. */
|
|
63
|
+
private processAccessibilityData;
|
|
64
|
+
/** Collects all A11y data sources (attachments + annotations) for a test. */
|
|
65
|
+
private collectA11yDataSources;
|
|
66
|
+
/** Attempts to parse a single A11y data source into a ReportData object. */
|
|
67
|
+
private parseA11ySource;
|
|
68
|
+
/** Generates a sanitized HTML filename for an accessibility report. */
|
|
69
|
+
private buildA11yReportName;
|
|
70
|
+
/** Applies reporter-level configuration (colors, ADO, video) to a ReportData object. */
|
|
71
|
+
private applyReportConfig;
|
|
72
|
+
/**
|
|
73
|
+
* Backfills reproduction steps from `test.step` calls into a11y targets
|
|
74
|
+
* that have no steps recorded (e.g. violations found via static scan).
|
|
75
|
+
*/
|
|
76
|
+
private backfillSteps;
|
|
77
|
+
/**
|
|
78
|
+
* Aggregates a11y error counts into both the browser-specific and global
|
|
79
|
+
* summaries. Returns the total error count for this scan.
|
|
80
|
+
*/
|
|
81
|
+
private aggregateA11yErrors;
|
|
82
|
+
/** Updates the browser-specific test counts (passed/failed/skipped). */
|
|
83
|
+
private updateBrowserSummary;
|
|
84
|
+
/** Updates the global execution summary counts. */
|
|
85
|
+
private updateGlobalSummary;
|
|
86
|
+
/** Ensures a file group key exists in the grouped results map. */
|
|
87
|
+
private ensureGroupExists;
|
|
88
|
+
/** Lazily initialises and returns the browser summary for the given browser name. */
|
|
89
|
+
private getOrCreateBrowserSummary;
|
|
44
90
|
}
|
|
45
91
|
export default SnapAllyReporter;
|