snap-ally 0.1.0-beta → 0.2.1-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 +47 -47
- package/dist/A11yAuditOverlay.d.ts +46 -17
- package/dist/A11yAuditOverlay.js +209 -101
- package/dist/A11yHtmlRenderer.js +11 -8
- package/dist/A11yReportAssets.js +16 -11
- package/dist/A11yScanner.js +24 -14
- package/dist/A11yTimeUtils.js +14 -1
- package/dist/SnapAllyReporter.d.ts +56 -10
- package/dist/SnapAllyReporter.js +314 -234
- package/dist/templates/accessibility-report.html +396 -260
- package/dist/templates/execution-summary.html +282 -193
- package/dist/templates/global-report-styles.css +945 -886
- package/dist/templates/report-app.js +862 -746
- package/dist/templates/test-execution-report.html +261 -188
- package/package.json +50 -41
package/README.md
CHANGED
|
@@ -37,29 +37,29 @@ npm install snap-ally --save-dev
|
|
|
37
37
|
Add `snap-ally` to your `playwright.config.ts`:
|
|
38
38
|
|
|
39
39
|
```typescript
|
|
40
|
-
import { defineConfig } from
|
|
40
|
+
import { defineConfig } from '@playwright/test';
|
|
41
41
|
|
|
42
42
|
export default defineConfig({
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
43
|
+
reporter: [
|
|
44
|
+
[
|
|
45
|
+
'snap-ally',
|
|
46
|
+
{
|
|
47
|
+
outputFolder: 'a11y-report',
|
|
48
|
+
// Optional: Visual Customization
|
|
49
|
+
colors: {
|
|
50
|
+
critical: '#dc2626',
|
|
51
|
+
serious: '#ea580c',
|
|
52
|
+
moderate: '#f59e0b',
|
|
53
|
+
minor: '#0ea5e9',
|
|
54
|
+
},
|
|
55
|
+
// Optional: Azure DevOps Integration
|
|
56
|
+
ado: {
|
|
57
|
+
organization: 'your-org',
|
|
58
|
+
project: 'your-project',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
61
62
|
],
|
|
62
|
-
],
|
|
63
63
|
});
|
|
64
64
|
```
|
|
65
65
|
|
|
@@ -70,25 +70,25 @@ export default defineConfig({
|
|
|
70
70
|
Import and use `scanA11y` within your Playwright tests:
|
|
71
71
|
|
|
72
72
|
```typescript
|
|
73
|
-
import { test } from
|
|
74
|
-
import { scanA11y } from
|
|
75
|
-
|
|
76
|
-
test(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
73
|
+
import { test } from '@playwright/test';
|
|
74
|
+
import { scanA11y } from 'snap-ally';
|
|
75
|
+
|
|
76
|
+
test('verify page accessibility', async ({ page }, testInfo) => {
|
|
77
|
+
await page.goto('https://example.com');
|
|
78
|
+
|
|
79
|
+
// Basic scan
|
|
80
|
+
await scanA11y(page, testInfo);
|
|
81
|
+
|
|
82
|
+
// Advanced scan with configuration
|
|
83
|
+
await scanA11y(page, testInfo, {
|
|
84
|
+
verbose: true, // Log results to terminal
|
|
85
|
+
consoleLog: true, // Log results to browser console
|
|
86
|
+
pageKey: 'Homepage', // Custom name for the report file
|
|
87
|
+
tags: ['wcag2a', 'wcag2aa'],
|
|
88
|
+
rules: {
|
|
89
|
+
'color-contrast': { enabled: false },
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
92
|
});
|
|
93
93
|
```
|
|
94
94
|
|
|
@@ -108,14 +108,14 @@ test("verify page accessibility", async ({ page }, testInfo) => {
|
|
|
108
108
|
|
|
109
109
|
### `scanA11y` Options
|
|
110
110
|
|
|
111
|
-
| Option
|
|
112
|
-
|
|
|
113
|
-
| `include`
|
|
114
|
-
| `verbose`
|
|
115
|
-
| `consoleLog` | `boolean`
|
|
116
|
-
| `rules`
|
|
117
|
-
| `tags`
|
|
118
|
-
| `pageKey`
|
|
111
|
+
| Option | Type | Description |
|
|
112
|
+
| ------------ | ---------- | -------------------------------------------------------------------------- |
|
|
113
|
+
| `include` | `string` | CSS selector to limit the scan to a specific element. |
|
|
114
|
+
| `verbose` | `boolean` | **Terminal Logs**: Print violations to terminal. Defaults to `true`. |
|
|
115
|
+
| `consoleLog` | `boolean` | **Browser Logs**: Print violations to browser console. Defaults to `true`. |
|
|
116
|
+
| `rules` | `object` | Axe-core rule configuration. |
|
|
117
|
+
| `tags` | `string[]` | List of Axe-core tags to run (e.g., `['wcag2aa']`). |
|
|
118
|
+
| `pageKey` | `string` | Custom identifier for the report file name. |
|
|
119
119
|
|
|
120
120
|
---
|
|
121
121
|
|
|
@@ -1,42 +1,71 @@
|
|
|
1
1
|
import { Page, TestInfo } from '@playwright/test';
|
|
2
|
-
export interface AuditAnnotation {
|
|
3
|
-
type: string;
|
|
4
|
-
description: string;
|
|
5
|
-
keyPage: string;
|
|
6
|
-
}
|
|
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
|
@@ -4,36 +4,58 @@ exports.A11yAuditOverlay = void 0;
|
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
/**
|
|
6
6
|
* Handles visual feedback and Playwright annotations during an accessibility audit.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities:
|
|
9
|
+
* - Rendering violation banners and element highlights via a Shadow DOM overlay
|
|
10
|
+
* - Capturing screenshots and attaching them to the Playwright test report
|
|
11
|
+
*
|
|
12
|
+
* All DOM mutations are isolated inside a Shadow DOM root to avoid
|
|
13
|
+
* interfering with the page under test.
|
|
7
14
|
*/
|
|
8
15
|
class A11yAuditOverlay {
|
|
9
|
-
constructor(page
|
|
16
|
+
constructor(page) {
|
|
10
17
|
this.page = page;
|
|
11
|
-
this.keyPage = keyPage;
|
|
12
18
|
this.overlayRootId = 'a11y-audit-overlay-root';
|
|
13
|
-
this.auditAnnotations = [];
|
|
14
|
-
}
|
|
15
|
-
reset() {
|
|
16
|
-
this.auditAnnotations = [];
|
|
17
19
|
}
|
|
20
|
+
// ──────────────────────────────────────────────
|
|
21
|
+
// Violation banner
|
|
22
|
+
// ──────────────────────────────────────────────
|
|
18
23
|
/**
|
|
19
24
|
* Shows a compact, modern banner at the bottom of the page describing the violation.
|
|
25
|
+
*
|
|
26
|
+
* @param violation - Object containing the Axe rule `id` and human-readable `help` text.
|
|
27
|
+
* @param color - CSS colour used as the banner background (rgb/rgba/hex).
|
|
20
28
|
*/
|
|
21
29
|
async showViolationOverlay(violation, color) {
|
|
22
|
-
await this.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
await this.safeEvaluate(([v, rawColor, rootId, bannerId]) => {
|
|
31
|
+
// --- helpers scoped to the browser context ---
|
|
32
|
+
const toAlphaColor = (c, alpha = 0.85) => {
|
|
33
|
+
if (c.includes('rgba'))
|
|
34
|
+
return c;
|
|
35
|
+
if (c.includes('rgb'))
|
|
36
|
+
return c.replace('rgb', 'rgba').replace(')', `, ${alpha})`);
|
|
37
|
+
// Hex – append alpha byte (0xD9 ≈ 0.85)
|
|
38
|
+
return `${c}D9`;
|
|
39
|
+
};
|
|
40
|
+
const getOrCreateRoot = (id) => {
|
|
41
|
+
let root = document.getElementById(id);
|
|
42
|
+
if (!root) {
|
|
43
|
+
root = document.createElement('div');
|
|
44
|
+
root.id = id;
|
|
45
|
+
root.style.cssText =
|
|
46
|
+
'position:absolute;top:0;left:0;width:0;height:0;z-index:2147483647;';
|
|
47
|
+
document.body.appendChild(root);
|
|
48
|
+
root.attachShadow({ mode: 'open' });
|
|
49
|
+
}
|
|
50
|
+
return root.shadowRoot;
|
|
51
|
+
};
|
|
52
|
+
// --- main logic ---
|
|
53
|
+
const shadow = getOrCreateRoot(rootId);
|
|
54
|
+
let container = shadow.getElementById(bannerId);
|
|
33
55
|
if (!container) {
|
|
34
56
|
const style = document.createElement('style');
|
|
35
57
|
style.textContent = `
|
|
36
|
-
|
|
58
|
+
#${bannerId} {
|
|
37
59
|
position: fixed;
|
|
38
60
|
left: 50%;
|
|
39
61
|
bottom: 24px;
|
|
@@ -55,7 +77,7 @@ class A11yAuditOverlay {
|
|
|
55
77
|
z-index: 10000;
|
|
56
78
|
}
|
|
57
79
|
.badge {
|
|
58
|
-
background: rgba(255,
|
|
80
|
+
background: rgba(255,255,255,0.2);
|
|
59
81
|
padding: 2px 8px;
|
|
60
82
|
border-radius: 6px;
|
|
61
83
|
font-size: 11px;
|
|
@@ -73,122 +95,208 @@ class A11yAuditOverlay {
|
|
|
73
95
|
`;
|
|
74
96
|
shadow.appendChild(style);
|
|
75
97
|
container = document.createElement('div');
|
|
76
|
-
container.id =
|
|
98
|
+
container.id = bannerId;
|
|
77
99
|
shadow.appendChild(container);
|
|
78
100
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
container.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
container.style.backgroundColor = toAlphaColor(rawColor);
|
|
102
|
+
// Build DOM nodes instead of innerHTML to prevent XSS
|
|
103
|
+
container.textContent = ''; // clear previous content
|
|
104
|
+
const icon = document.createElement('div');
|
|
105
|
+
icon.style.fontSize = '20px';
|
|
106
|
+
icon.textContent = '⚠️';
|
|
107
|
+
const badge = document.createElement('span');
|
|
108
|
+
badge.className = 'badge';
|
|
109
|
+
badge.textContent = v.id;
|
|
110
|
+
const helpText = document.createElement('span');
|
|
111
|
+
helpText.style.opacity = '0.9';
|
|
112
|
+
helpText.textContent = v.help;
|
|
113
|
+
const row = document.createElement('div');
|
|
114
|
+
row.style.cssText = 'margin-bottom:4px;display:flex;align-items:center;gap:8px;';
|
|
115
|
+
row.appendChild(badge);
|
|
116
|
+
row.appendChild(helpText);
|
|
117
|
+
const content = document.createElement('div');
|
|
118
|
+
content.className = 'content';
|
|
119
|
+
content.appendChild(row);
|
|
120
|
+
container.appendChild(icon);
|
|
121
|
+
container.appendChild(content);
|
|
122
|
+
}, [
|
|
123
|
+
violation,
|
|
124
|
+
color,
|
|
125
|
+
this.overlayRootId,
|
|
126
|
+
A11yAuditOverlay.BANNER_ID,
|
|
127
|
+
]);
|
|
92
128
|
}
|
|
93
129
|
/**
|
|
94
|
-
* Removes the violation description
|
|
130
|
+
* Removes the violation description banner from the page.
|
|
95
131
|
*/
|
|
96
132
|
async hideViolationOverlay() {
|
|
97
|
-
await this.
|
|
133
|
+
await this.safeEvaluate((rootId) => {
|
|
98
134
|
const el = document.getElementById(rootId);
|
|
99
135
|
if (el)
|
|
100
136
|
el.remove();
|
|
101
137
|
}, this.overlayRootId);
|
|
102
138
|
}
|
|
139
|
+
// ──────────────────────────────────────────────
|
|
140
|
+
// Element highlighting
|
|
141
|
+
// ──────────────────────────────────────────────
|
|
103
142
|
/**
|
|
104
|
-
*
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
getAuditAnnotations() {
|
|
113
|
-
return this.auditAnnotations;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Captures a screenshot and attaches it to the test report.
|
|
143
|
+
* Draws a glowing highlight border around the element matching `selector`.
|
|
144
|
+
*
|
|
145
|
+
* The element is scrolled into view first so that the highlight coordinates
|
|
146
|
+
* are accurate after any layout shift.
|
|
147
|
+
*
|
|
148
|
+
* @param selector - A CSS selector that uniquely identifies the target element.
|
|
149
|
+
* @param color - CSS colour for the highlight border and glow.
|
|
117
150
|
*/
|
|
118
|
-
async captureAndAttachScreenshot(fileName, testInfo) {
|
|
119
|
-
return await test_1.test.step('Capture A11y screenshot', async () => {
|
|
120
|
-
// Use viewport screenshot instead of fullPage to avoid browser resizing flashes
|
|
121
|
-
const screenshot = await this.page.screenshot({ fullPage: false });
|
|
122
|
-
await testInfo.attach(fileName, { contentType: 'image/png', body: screenshot });
|
|
123
|
-
return screenshot;
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
151
|
async highlightElement(selector, color) {
|
|
127
|
-
await this.
|
|
152
|
+
await this.safeEvaluate(([sel, rawColor, rootId, highlightId]) => {
|
|
128
153
|
const target = document.querySelector(sel);
|
|
129
154
|
if (!target)
|
|
130
155
|
return;
|
|
131
|
-
// Scroll FIRST to ensure accurate coordinates after scroll
|
|
132
156
|
target.scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
157
|
+
const toAlphaColor = (c, alpha) => {
|
|
158
|
+
if (c.includes('rgba'))
|
|
159
|
+
return c.replace(/[\d.]+\)$/, `${alpha})`);
|
|
160
|
+
if (c.includes('rgb'))
|
|
161
|
+
return c.replace('rgb', 'rgba').replace(')', `, ${alpha})`);
|
|
162
|
+
// Hex – convert alpha to 2-char hex
|
|
163
|
+
const hex = Math.round(alpha * 255)
|
|
164
|
+
.toString(16)
|
|
165
|
+
.padStart(2, '0');
|
|
166
|
+
return `${c}${hex}`;
|
|
167
|
+
};
|
|
168
|
+
const getOrCreateRoot = (id) => {
|
|
169
|
+
let root = document.getElementById(id);
|
|
170
|
+
if (!root) {
|
|
171
|
+
root = document.createElement('div');
|
|
172
|
+
root.id = id;
|
|
173
|
+
root.style.cssText =
|
|
174
|
+
'position:absolute;top:0;left:0;width:0;height:0;z-index:2147483647;';
|
|
175
|
+
document.body.appendChild(root);
|
|
176
|
+
root.attachShadow({ mode: 'open' });
|
|
177
|
+
}
|
|
178
|
+
return root.shadowRoot;
|
|
179
|
+
};
|
|
180
|
+
const shadow = getOrCreateRoot(rootId);
|
|
181
|
+
let highlight = shadow.getElementById(highlightId);
|
|
143
182
|
if (!highlight) {
|
|
144
183
|
const style = document.createElement('style');
|
|
145
184
|
style.textContent = `
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
border: 2px solid var(--c);
|
|
162
|
-
}
|
|
163
|
-
`;
|
|
185
|
+
#${highlightId} {
|
|
186
|
+
position: absolute;
|
|
187
|
+
pointer-events: none;
|
|
188
|
+
border-radius: 8px;
|
|
189
|
+
box-sizing: border-box;
|
|
190
|
+
z-index: 9999;
|
|
191
|
+
box-shadow: 0 0 0 4px var(--c-alpha), 0 0 20px var(--c-alpha);
|
|
192
|
+
}
|
|
193
|
+
.glow {
|
|
194
|
+
position: absolute;
|
|
195
|
+
inset: 0;
|
|
196
|
+
border-radius: inherit;
|
|
197
|
+
border: 2px solid var(--c);
|
|
198
|
+
}
|
|
199
|
+
`;
|
|
164
200
|
shadow.appendChild(style);
|
|
165
201
|
highlight = document.createElement('div');
|
|
166
|
-
highlight.id =
|
|
167
|
-
|
|
202
|
+
highlight.id = highlightId;
|
|
203
|
+
const glow = document.createElement('div');
|
|
204
|
+
glow.className = 'glow';
|
|
205
|
+
highlight.appendChild(glow);
|
|
168
206
|
shadow.appendChild(highlight);
|
|
169
207
|
}
|
|
208
|
+
const pad = 4;
|
|
170
209
|
const rect = target.getBoundingClientRect();
|
|
171
|
-
highlight.style.left = `${rect.left + window.scrollX -
|
|
172
|
-
highlight.style.top = `${rect.top + window.scrollY -
|
|
173
|
-
highlight.style.width = `${rect.width +
|
|
174
|
-
highlight.style.height = `${rect.height +
|
|
175
|
-
highlight.style.border = `3px solid ${
|
|
176
|
-
highlight.style.setProperty('--c',
|
|
177
|
-
highlight.style.setProperty('--c-alpha',
|
|
178
|
-
}, [
|
|
210
|
+
highlight.style.left = `${rect.left + window.scrollX - pad}px`;
|
|
211
|
+
highlight.style.top = `${rect.top + window.scrollY - pad}px`;
|
|
212
|
+
highlight.style.width = `${rect.width + pad * 2}px`;
|
|
213
|
+
highlight.style.height = `${rect.height + pad * 2}px`;
|
|
214
|
+
highlight.style.border = `3px solid ${rawColor}`;
|
|
215
|
+
highlight.style.setProperty('--c', rawColor);
|
|
216
|
+
highlight.style.setProperty('--c-alpha', toAlphaColor(rawColor, 0.3));
|
|
217
|
+
}, [
|
|
218
|
+
selector,
|
|
219
|
+
color,
|
|
220
|
+
this.overlayRootId,
|
|
221
|
+
A11yAuditOverlay.HIGHLIGHT_ID,
|
|
222
|
+
]);
|
|
179
223
|
}
|
|
180
224
|
/**
|
|
181
|
-
* Removes
|
|
225
|
+
* Removes the element highlight from the page.
|
|
182
226
|
*/
|
|
183
227
|
async unhighlightElement() {
|
|
184
|
-
await this.
|
|
228
|
+
await this.safeEvaluate(([rootId, highlightId]) => {
|
|
185
229
|
const root = document.getElementById(rootId);
|
|
186
|
-
if (root
|
|
187
|
-
const highlight = root.shadowRoot.getElementById(
|
|
230
|
+
if (root === null || root === void 0 ? void 0 : root.shadowRoot) {
|
|
231
|
+
const highlight = root.shadowRoot.getElementById(highlightId);
|
|
188
232
|
if (highlight)
|
|
189
233
|
highlight.remove();
|
|
190
234
|
}
|
|
191
|
-
}, this.overlayRootId);
|
|
235
|
+
}, [this.overlayRootId, A11yAuditOverlay.HIGHLIGHT_ID]);
|
|
236
|
+
}
|
|
237
|
+
// ──────────────────────────────────────────────
|
|
238
|
+
// Test report helpers
|
|
239
|
+
// ──────────────────────────────────────────────
|
|
240
|
+
/**
|
|
241
|
+
* Attaches arbitrary data to the Playwright test report.
|
|
242
|
+
*
|
|
243
|
+
* @param testInfo - The current Playwright `TestInfo` instance.
|
|
244
|
+
* @param name - Attachment name shown in the report.
|
|
245
|
+
* @param description - Content to attach (serialised as JSON by the caller).
|
|
246
|
+
*/
|
|
247
|
+
async addTestAttachment(testInfo, name, description) {
|
|
248
|
+
await testInfo.attach(name, {
|
|
249
|
+
contentType: 'application/json',
|
|
250
|
+
body: Buffer.from(description),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Captures a viewport screenshot and attaches it to the test report.
|
|
255
|
+
*
|
|
256
|
+
* The screenshot uses `fullPage: false` (viewport only) to avoid browser
|
|
257
|
+
* resizing flashes that can occur with full-page captures.
|
|
258
|
+
*
|
|
259
|
+
* @param fileName - Name for the attachment in the test report.
|
|
260
|
+
* @param testInfo - The current Playwright `TestInfo` instance.
|
|
261
|
+
* @returns The raw screenshot buffer.
|
|
262
|
+
*/
|
|
263
|
+
async captureAndAttachScreenshot(fileName, testInfo) {
|
|
264
|
+
return await test_1.test.step('Capture A11y screenshot', async () => {
|
|
265
|
+
const screenshot = await this.page.screenshot({ fullPage: false });
|
|
266
|
+
await testInfo.attach(fileName, { contentType: 'image/png', body: screenshot });
|
|
267
|
+
return screenshot;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// ──────────────────────────────────────────────
|
|
271
|
+
// Private helpers
|
|
272
|
+
// ──────────────────────────────────────────────
|
|
273
|
+
/**
|
|
274
|
+
* Wrapper around `page.evaluate` that silently swallows errors caused by
|
|
275
|
+
* the page or browser closing mid-evaluation (e.g. navigation, test teardown).
|
|
276
|
+
*/
|
|
277
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
278
|
+
async safeEvaluate(fn, arg) {
|
|
279
|
+
try {
|
|
280
|
+
if (arg !== undefined) {
|
|
281
|
+
await this.page.evaluate(fn, arg);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
await this.page.evaluate(fn);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
if (error instanceof Error &&
|
|
289
|
+
(error.message.includes('Target page, context or browser has been closed') ||
|
|
290
|
+
error.message.includes('Execution context was destroyed') ||
|
|
291
|
+
error.message.includes('Test ended'))) {
|
|
292
|
+
// Page navigated away or test ended — overlay is no longer relevant.
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
192
297
|
}
|
|
193
298
|
}
|
|
194
299
|
exports.A11yAuditOverlay = A11yAuditOverlay;
|
|
300
|
+
/** IDs for elements created inside the shadow root. */
|
|
301
|
+
A11yAuditOverlay.BANNER_ID = 'a11y-banner';
|
|
302
|
+
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
|
|
@@ -102,10 +108,7 @@ class A11yHtmlRenderer {
|
|
|
102
108
|
'\u001b[22m': '</span>',
|
|
103
109
|
'\u001b[39m': '</span>',
|
|
104
110
|
};
|
|
105
|
-
let result = text
|
|
106
|
-
.replace(/&/g, '&')
|
|
107
|
-
.replace(/</g, '<')
|
|
108
|
-
.replace(/>/g, '>');
|
|
111
|
+
let result = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
109
112
|
for (const [code, tag] of Object.entries(map)) {
|
|
110
113
|
result = result.split(code).join(tag);
|
|
111
114
|
}
|