snap-ally 0.0.4 → 0.1.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.js +10 -5
- package/dist/A11yHtmlRenderer.d.ts +3 -3
- package/dist/A11yHtmlRenderer.js +32 -22
- package/dist/A11yReportAssets.d.ts +1 -1
- package/dist/A11yReportAssets.js +31 -18
- package/dist/A11yScanner.js +39 -16
- package/dist/A11yTimeUtils.d.ts +4 -0
- package/dist/A11yTimeUtils.js +28 -0
- package/dist/SnapAllyReporter.js +86 -50
- package/dist/models/index.d.ts +11 -1
- package/dist/templates/accessibility-report.html +393 -1376
- package/dist/templates/execution-summary.html +269 -669
- package/dist/templates/global-report-styles.css +1595 -0
- package/dist/templates/report-app.js +940 -0
- package/dist/templates/test-execution-report.html +257 -569
- package/package.json +50 -42
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
|
|
package/dist/A11yAuditOverlay.js
CHANGED
|
@@ -24,7 +24,8 @@ class A11yAuditOverlay {
|
|
|
24
24
|
if (!root) {
|
|
25
25
|
root = document.createElement('div');
|
|
26
26
|
root.id = rootId;
|
|
27
|
-
root.style.cssText =
|
|
27
|
+
root.style.cssText =
|
|
28
|
+
'position: absolute; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647;';
|
|
28
29
|
document.body.appendChild(root);
|
|
29
30
|
root.attachShadow({ mode: 'open' });
|
|
30
31
|
}
|
|
@@ -76,8 +77,11 @@ class A11yAuditOverlay {
|
|
|
76
77
|
container.id = 'a11y-banner';
|
|
77
78
|
shadow.appendChild(container);
|
|
78
79
|
}
|
|
79
|
-
const alphaColor = color.includes('rgba')
|
|
80
|
-
|
|
80
|
+
const alphaColor = color.includes('rgba')
|
|
81
|
+
? color
|
|
82
|
+
: color.includes('rgb')
|
|
83
|
+
? color.replace('rgb', 'rgba').replace(')', ', 0.85)')
|
|
84
|
+
: color + 'E6';
|
|
81
85
|
container.style.backgroundColor = alphaColor;
|
|
82
86
|
container.innerHTML = `
|
|
83
87
|
<div style="font-size: 20px;">⚠️</div>
|
|
@@ -106,7 +110,7 @@ class A11yAuditOverlay {
|
|
|
106
110
|
async addTestAttachment(testInfo, name, description) {
|
|
107
111
|
await testInfo.attach(name, {
|
|
108
112
|
contentType: 'application/json',
|
|
109
|
-
body: Buffer.from(description)
|
|
113
|
+
body: Buffer.from(description),
|
|
110
114
|
});
|
|
111
115
|
}
|
|
112
116
|
getAuditAnnotations() {
|
|
@@ -134,7 +138,8 @@ class A11yAuditOverlay {
|
|
|
134
138
|
if (!root) {
|
|
135
139
|
root = document.createElement('div');
|
|
136
140
|
root.id = rootId;
|
|
137
|
-
root.style.cssText =
|
|
141
|
+
root.style.cssText =
|
|
142
|
+
'position: absolute; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647;';
|
|
138
143
|
document.body.appendChild(root);
|
|
139
144
|
root.attachShadow({ mode: 'open' });
|
|
140
145
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Handles the rendering of HTML reports using
|
|
2
|
+
* Handles the rendering of HTML reports using static templates and JSON data injection.
|
|
3
3
|
*/
|
|
4
4
|
export declare class A11yHtmlRenderer {
|
|
5
5
|
/**
|
|
6
|
-
* Renders
|
|
6
|
+
* Renders a static HTML template by copying it and generating the accompanied data payload.
|
|
7
7
|
* @param templateName The template file name in the templates folder.
|
|
8
|
-
* @param data The data object to pass to
|
|
8
|
+
* @param data The data object to pass to the client-side JS app.
|
|
9
9
|
* @param outputFolder The folder where the rendered file will be saved.
|
|
10
10
|
* @param outputFileName The full path of the output file.
|
|
11
11
|
*/
|
package/dist/A11yHtmlRenderer.js
CHANGED
|
@@ -36,40 +36,53 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.A11yHtmlRenderer = void 0;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
-
const ejs = __importStar(require("ejs"));
|
|
40
39
|
/**
|
|
41
|
-
* Handles the rendering of HTML reports using
|
|
40
|
+
* Handles the rendering of HTML reports using static templates and JSON data injection.
|
|
42
41
|
*/
|
|
43
42
|
class A11yHtmlRenderer {
|
|
44
43
|
/**
|
|
45
|
-
* Renders
|
|
44
|
+
* Renders a static HTML template by copying it and generating the accompanied data payload.
|
|
46
45
|
* @param templateName The template file name in the templates folder.
|
|
47
|
-
* @param data The data object to pass to
|
|
46
|
+
* @param data The data object to pass to the client-side JS app.
|
|
48
47
|
* @param outputFolder The folder where the rendered file will be saved.
|
|
49
48
|
* @param outputFileName The full path of the output file.
|
|
50
49
|
*/
|
|
51
50
|
async render(templateName, data, outputFolder, outputFileName) {
|
|
52
51
|
// Resolve path relative to this file (dist/A11yHtmlRenderer.js)
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
catch {
|
|
52
|
+
const templatesDir = path.join(__dirname, 'templates');
|
|
53
|
+
const templatePath = path.join(templatesDir, templateName);
|
|
54
|
+
const cssPath = path.join(templatesDir, 'global-report-styles.css');
|
|
55
|
+
const jsPath = path.join(templatesDir, 'report-app.js');
|
|
56
|
+
if (!fs.existsSync(templatePath)) {
|
|
59
57
|
throw new Error(`[A11yHtmlRenderer] Template not found: ${templatePath}`);
|
|
60
58
|
}
|
|
61
|
-
|
|
59
|
+
if (!fs.existsSync(outputFolder)) {
|
|
60
|
+
fs.mkdirSync(outputFolder, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
// 1. Copy the pure HTML template to the output location
|
|
63
|
+
fs.copyFileSync(templatePath, outputFileName);
|
|
64
|
+
// 2. Wrap the report data in a JS variable and write data.js next to the HTML file
|
|
65
|
+
const outputDir = path.dirname(outputFileName);
|
|
66
|
+
const dataJsPath = path.join(outputDir, 'data.js');
|
|
67
|
+
const jsContent = `window.snapAllyData = ${JSON.stringify(data)};`;
|
|
68
|
+
fs.writeFileSync(dataJsPath, jsContent, 'utf8');
|
|
69
|
+
// 3. Copy the global CSS and JS rendering engine next to the HTML file
|
|
70
|
+
const outCssPath = path.join(outputDir, 'global-report-styles.css');
|
|
71
|
+
const outJsPath = path.join(outputDir, 'report-app.js');
|
|
62
72
|
try {
|
|
63
|
-
|
|
73
|
+
if (fs.existsSync(cssPath))
|
|
74
|
+
fs.copyFileSync(cssPath, outCssPath);
|
|
64
75
|
}
|
|
65
|
-
catch (
|
|
66
|
-
console.error(
|
|
67
|
-
throw error;
|
|
76
|
+
catch (e) {
|
|
77
|
+
console.error('Error copying CSS:', e);
|
|
68
78
|
}
|
|
69
|
-
|
|
70
|
-
fs.
|
|
79
|
+
try {
|
|
80
|
+
if (fs.existsSync(jsPath))
|
|
81
|
+
fs.copyFileSync(jsPath, outJsPath);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
console.error('Error copying JS:', e);
|
|
71
85
|
}
|
|
72
|
-
fs.writeFileSync(outputFileName, html);
|
|
73
86
|
}
|
|
74
87
|
/**
|
|
75
88
|
* Converts ANSI color codes to HTML spans for nicer error display.
|
|
@@ -89,10 +102,7 @@ class A11yHtmlRenderer {
|
|
|
89
102
|
'\u001b[22m': '</span>',
|
|
90
103
|
'\u001b[39m': '</span>',
|
|
91
104
|
};
|
|
92
|
-
let result = text
|
|
93
|
-
.replace(/&/g, '&')
|
|
94
|
-
.replace(/</g, '<')
|
|
95
|
-
.replace(/>/g, '>');
|
|
105
|
+
let result = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
96
106
|
for (const [code, tag] of Object.entries(map)) {
|
|
97
107
|
result = result.split(code).join(tag);
|
|
98
108
|
}
|
|
@@ -9,7 +9,7 @@ export declare class A11yReportAssets {
|
|
|
9
9
|
copyToFolder(destFolder: string, srcPath: string, fileName?: string): string;
|
|
10
10
|
/**
|
|
11
11
|
* Copies the test video if available.
|
|
12
|
-
* Includes a
|
|
12
|
+
* Includes a more robust retry to ensure Playwright has finished flushing the file.
|
|
13
13
|
*/
|
|
14
14
|
copyTestVideo(result: TestResult, destFolder: string): Promise<string>;
|
|
15
15
|
/**
|
package/dist/A11yReportAssets.js
CHANGED
|
@@ -57,31 +57,33 @@ class A11yReportAssets {
|
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* Copies the test video if available.
|
|
60
|
-
* Includes a
|
|
60
|
+
* Includes a more robust retry to ensure Playwright has finished flushing the file.
|
|
61
61
|
*/
|
|
62
62
|
async copyTestVideo(result, destFolder) {
|
|
63
|
-
|
|
63
|
+
// More flexible matching for video attachments
|
|
64
|
+
const videoAttachments = result.attachments.filter((a) => a.name === 'video' || (a.contentType || '').startsWith('video/'));
|
|
64
65
|
let bestVideo = null;
|
|
65
66
|
let maxSize = -1;
|
|
66
67
|
for (const attachment of videoAttachments) {
|
|
67
68
|
if (!attachment.path)
|
|
68
69
|
continue;
|
|
69
|
-
// Retry logic: Wait for file to exist and have non-zero size (up to
|
|
70
|
+
// Retry logic: Wait for file to exist and have non-zero size (up to 3 seconds)
|
|
70
71
|
let attempts = 0;
|
|
71
72
|
let isReady = false;
|
|
72
|
-
while (attempts <
|
|
73
|
+
while (attempts < 15) {
|
|
73
74
|
if (fs.existsSync(attachment.path)) {
|
|
74
75
|
try {
|
|
75
|
-
|
|
76
|
+
const stats = fs.statSync(attachment.path);
|
|
77
|
+
if (stats.size > 0) {
|
|
76
78
|
isReady = true;
|
|
77
79
|
break;
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
|
-
catch
|
|
82
|
+
catch {
|
|
81
83
|
// statSync might fail if file is temporarily locked
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
|
-
await new Promise(r => setTimeout(r, 200));
|
|
86
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
85
87
|
attempts++;
|
|
86
88
|
}
|
|
87
89
|
if (isReady) {
|
|
@@ -97,12 +99,12 @@ class A11yReportAssets {
|
|
|
97
99
|
}
|
|
98
100
|
}
|
|
99
101
|
else {
|
|
100
|
-
console.warn(`[SnapAlly] Video attachment found but file is missing or empty: ${attachment.path}`);
|
|
102
|
+
console.warn(`[SnapAlly] Video attachment found but file is missing or empty after retry: ${attachment.path}`);
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
if (bestVideo) {
|
|
104
106
|
try {
|
|
105
|
-
return this.copyToFolder(destFolder, bestVideo);
|
|
107
|
+
return this.copyToFolder(destFolder, bestVideo, 'video.webm');
|
|
106
108
|
}
|
|
107
109
|
catch (e) {
|
|
108
110
|
console.error(`[SnapAlly] Failed to copy video: ${e}`);
|
|
@@ -116,31 +118,39 @@ class A11yReportAssets {
|
|
|
116
118
|
*/
|
|
117
119
|
copyScreenshots(result, destFolder) {
|
|
118
120
|
return result.attachments
|
|
119
|
-
.filter(a => a.name === 'screenshot')
|
|
120
|
-
.map(a => {
|
|
121
|
+
.filter((a) => a.name === 'screenshot' || (a.contentType || '').startsWith('image/'))
|
|
122
|
+
.map((a) => {
|
|
121
123
|
if (a.path) {
|
|
122
124
|
return this.copyToFolder(destFolder, a.path);
|
|
123
125
|
}
|
|
124
126
|
else if (a.body) {
|
|
125
|
-
|
|
127
|
+
const timestamp = Date.now();
|
|
128
|
+
const name = a.name === 'screenshot'
|
|
129
|
+
? `screenshot-${timestamp}.png`
|
|
130
|
+
: a.name.endsWith('.png')
|
|
131
|
+
? a.name
|
|
132
|
+
: `${a.name}.png`;
|
|
133
|
+
return this.writeBuffer(destFolder, name, a.body);
|
|
126
134
|
}
|
|
127
135
|
return '';
|
|
128
136
|
})
|
|
129
|
-
.filter(path => path !== '');
|
|
137
|
+
.filter((path) => path !== '');
|
|
130
138
|
}
|
|
131
139
|
/**
|
|
132
140
|
* Copies all PNG attachments to the report folder and returns their new names.
|
|
133
141
|
*/
|
|
134
142
|
copyPngAttachments(result, destFolder) {
|
|
135
143
|
return result.attachments
|
|
136
|
-
.filter(a => a.name.endsWith('.png')
|
|
137
|
-
.
|
|
144
|
+
.filter((a) => (a.name.endsWith('.png') || (a.contentType || '') === 'image/png') &&
|
|
145
|
+
a.name !== 'screenshot')
|
|
146
|
+
.map((a) => {
|
|
138
147
|
let name = '';
|
|
139
148
|
if (a.path) {
|
|
140
149
|
name = this.copyToFolder(destFolder, a.path, a.name);
|
|
141
150
|
}
|
|
142
151
|
else if (a.body) {
|
|
143
|
-
|
|
152
|
+
const safeName = a.name.endsWith('.png') ? a.name : `${a.name}.png`;
|
|
153
|
+
name = this.writeBuffer(destFolder, safeName, a.body);
|
|
144
154
|
}
|
|
145
155
|
return name ? { path: name, name: a.name } : null;
|
|
146
156
|
})
|
|
@@ -152,8 +162,11 @@ class A11yReportAssets {
|
|
|
152
162
|
copyAllOtherAttachments(result, destFolder) {
|
|
153
163
|
const excludedNames = ['screenshot', 'video', 'A11y'];
|
|
154
164
|
return result.attachments
|
|
155
|
-
.filter(a => !excludedNames.includes(a.name) &&
|
|
156
|
-
.
|
|
165
|
+
.filter((a) => !excludedNames.includes(a.name) &&
|
|
166
|
+
!a.name.endsWith('.png') &&
|
|
167
|
+
!(a.contentType || '').startsWith('image/') &&
|
|
168
|
+
!(a.contentType || '').startsWith('video/'))
|
|
169
|
+
.map((a) => {
|
|
157
170
|
let name = '';
|
|
158
171
|
if (a.path) {
|
|
159
172
|
name = this.copyToFolder(destFolder, a.path, a.name);
|
package/dist/A11yScanner.js
CHANGED
|
@@ -9,12 +9,13 @@ const playwright_1 = __importDefault(require("@axe-core/playwright"));
|
|
|
9
9
|
const test_1 = require("@playwright/test");
|
|
10
10
|
const A11yAuditOverlay_1 = require("./A11yAuditOverlay");
|
|
11
11
|
const models_1 = require("./models");
|
|
12
|
+
const A11yTimeUtils_1 = require("./A11yTimeUtils");
|
|
12
13
|
/**
|
|
13
14
|
* Sanitizes a string to be safe for use in file paths and prevents path traversal attacks.
|
|
14
15
|
* Removes or replaces dangerous characters and path separators.
|
|
15
16
|
*/
|
|
16
17
|
function sanitizePageKey(input) {
|
|
17
|
-
return input
|
|
18
|
+
return (input
|
|
18
19
|
// Remove protocol
|
|
19
20
|
.replace(/^https?:\/\//, '')
|
|
20
21
|
// Remove or replace path separators and dangerous characters
|
|
@@ -28,7 +29,7 @@ function sanitizePageKey(input) {
|
|
|
28
29
|
// Convert to lowercase for consistency
|
|
29
30
|
.toLowerCase()
|
|
30
31
|
// Limit length to prevent filesystem issues
|
|
31
|
-
.substring(0, 200);
|
|
32
|
+
.substring(0, 200));
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
35
|
* Performs an accessibility audit using Axe and Lighthouse.
|
|
@@ -50,6 +51,7 @@ async function scanA11y(page, testInfo, options = {}) {
|
|
|
50
51
|
}
|
|
51
52
|
else {
|
|
52
53
|
// AxeBuilder for playwright also supports locators/elements in include
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
55
|
axeBuilder = axeBuilder.include(target);
|
|
54
56
|
}
|
|
55
57
|
}
|
|
@@ -62,7 +64,19 @@ async function scanA11y(page, testInfo, options = {}) {
|
|
|
62
64
|
if (options.axeOptions) {
|
|
63
65
|
axeBuilder = axeBuilder.options(options.axeOptions);
|
|
64
66
|
}
|
|
65
|
-
|
|
67
|
+
let axeResults;
|
|
68
|
+
try {
|
|
69
|
+
axeResults = await axeBuilder.analyze();
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof Error &&
|
|
73
|
+
(error.message.includes('Test ended') ||
|
|
74
|
+
error.message.includes('Target page, context or browser has been closed'))) {
|
|
75
|
+
console.warn(`[SnapAlly] Accessibility scan skipped: ${error.message}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
66
80
|
const violationCount = axeResults.violations.length;
|
|
67
81
|
if ((showTerminal || showBrowser) && violationCount > 0) {
|
|
68
82
|
const mainMsg = `[A11yScanner] Violations found: ${violationCount}`;
|
|
@@ -71,7 +85,7 @@ async function scanA11y(page, testInfo, options = {}) {
|
|
|
71
85
|
// Log to terminal
|
|
72
86
|
if (showTerminal) {
|
|
73
87
|
console.log(`\n${mainMsg}`);
|
|
74
|
-
detailMessages.forEach(msg => console.log(msg));
|
|
88
|
+
detailMessages.forEach((msg) => console.log(msg));
|
|
75
89
|
}
|
|
76
90
|
// Batch log to Browser Console in a single evaluate call
|
|
77
91
|
if (showBrowser) {
|
|
@@ -82,14 +96,16 @@ async function scanA11y(page, testInfo, options = {}) {
|
|
|
82
96
|
}
|
|
83
97
|
}
|
|
84
98
|
// Fail the test if violations found (softly)
|
|
85
|
-
test_1.expect
|
|
99
|
+
test_1.expect
|
|
100
|
+
.soft(violationCount, `Accessibility audit failed with ${violationCount} violations.`)
|
|
101
|
+
.toBe(0);
|
|
86
102
|
// Run Axe Audit
|
|
87
103
|
const errors = [];
|
|
88
104
|
const colorMap = {
|
|
89
105
|
minor: '#0ea5e9', // Ocean Blue
|
|
90
106
|
moderate: '#f59e0b', // Amber/Honey
|
|
91
107
|
serious: '#ea580c', // Deep Orange
|
|
92
|
-
critical: '#dc2626' // Power Red
|
|
108
|
+
critical: '#dc2626', // Power Red
|
|
93
109
|
};
|
|
94
110
|
// Process violations for the report
|
|
95
111
|
for (const violation of axeResults.violations) {
|
|
@@ -103,16 +119,21 @@ async function scanA11y(page, testInfo, options = {}) {
|
|
|
103
119
|
await overlay.showViolationOverlay({ id: violation.id, help: violation.help }, severityColor);
|
|
104
120
|
if (await locator.isVisible()) {
|
|
105
121
|
await overlay.highlightElement(elementSelector, severityColor);
|
|
106
|
-
// Allow time for
|
|
122
|
+
// Allow a small time for overlay highlight to be visible in video
|
|
107
123
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
108
|
-
await page.waitForTimeout(
|
|
124
|
+
await page.waitForTimeout(100);
|
|
109
125
|
const screenshotName = `a11y-${violation.id}-${errorIdx++}.png`;
|
|
110
126
|
const buffer = await overlay.captureAndAttachScreenshot(screenshotName, testInfo);
|
|
111
127
|
// Capture execution steps for context
|
|
112
|
-
const excluded = new Set([
|
|
128
|
+
const excluded = new Set([
|
|
129
|
+
'Pre Condition',
|
|
130
|
+
'Post Condition',
|
|
131
|
+
'Description',
|
|
132
|
+
'A11y',
|
|
133
|
+
]);
|
|
113
134
|
const contextSteps = (testInfo.annotations || [])
|
|
114
|
-
.filter(a => !excluded.has(a.type))
|
|
115
|
-
.map(a => a.description || '');
|
|
135
|
+
.filter((a) => !excluded.has(a.type))
|
|
136
|
+
.map((a) => a.description || '');
|
|
116
137
|
const nodeHtml = node.html || '';
|
|
117
138
|
const friendlySnippet = elementSelector; // Use full CSS selector path from Axe core
|
|
118
139
|
targets.push({
|
|
@@ -122,7 +143,7 @@ async function scanA11y(page, testInfo, options = {}) {
|
|
|
122
143
|
screenshot: screenshotName,
|
|
123
144
|
steps: contextSteps,
|
|
124
145
|
stepsJson: JSON.stringify(contextSteps),
|
|
125
|
-
screenshotBase64: buffer.toString('base64')
|
|
146
|
+
screenshotBase64: buffer.toString('base64'),
|
|
126
147
|
});
|
|
127
148
|
await overlay.unhighlightElement();
|
|
128
149
|
}
|
|
@@ -135,23 +156,25 @@ async function scanA11y(page, testInfo, options = {}) {
|
|
|
135
156
|
helpUrl: violation.helpUrl,
|
|
136
157
|
help: violation.help,
|
|
137
158
|
guideline: violation.tags[1] || 'N/A',
|
|
138
|
-
wcagRule: violation.tags.find(t => t.startsWith('wcag')) || violation.tags[1] || 'N/A',
|
|
159
|
+
wcagRule: violation.tags.find((t) => t.startsWith('wcag')) || violation.tags[1] || 'N/A',
|
|
139
160
|
total: targets.length || violation.nodes.length, // Fallback to node count if no screenshots
|
|
140
|
-
target: targets
|
|
161
|
+
target: targets,
|
|
141
162
|
});
|
|
142
163
|
}
|
|
143
164
|
// Prepare data for the reporter
|
|
144
165
|
const reportData = {
|
|
145
166
|
pageKey,
|
|
167
|
+
pageUrl: page.url(),
|
|
146
168
|
accessibilityScore: 0, // No longer used, derivation from Lighthouse removed
|
|
147
|
-
errors,
|
|
169
|
+
a11yErrors: errors,
|
|
148
170
|
video: 'a11y-scan-video.webm', // Reference name for reporter
|
|
149
171
|
criticalColor: models_1.Severity.critical,
|
|
150
172
|
seriousColor: models_1.Severity.serious,
|
|
151
173
|
moderateColor: models_1.Severity.moderate,
|
|
152
174
|
minorColor: models_1.Severity.minor,
|
|
153
175
|
adoOrganization: process.env.ADO_ORGANIZATION || '',
|
|
154
|
-
adoProject: process.env.ADO_PROJECT || ''
|
|
176
|
+
adoProject: process.env.ADO_PROJECT || '',
|
|
177
|
+
timestamp: A11yTimeUtils_1.A11yTimeUtils.formatDate(new Date()),
|
|
155
178
|
};
|
|
156
179
|
await overlay.addTestAttachment(testInfo, 'A11y', JSON.stringify(reportData));
|
|
157
180
|
await overlay.hideViolationOverlay();
|
package/dist/A11yTimeUtils.d.ts
CHANGED
package/dist/A11yTimeUtils.js
CHANGED
|
@@ -20,5 +20,33 @@ class A11yTimeUtils {
|
|
|
20
20
|
const remainingSeconds = seconds % 60;
|
|
21
21
|
return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Formats a Date object into a human-readable string.
|
|
25
|
+
*/
|
|
26
|
+
static formatDate(date) {
|
|
27
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
28
|
+
const monthNames = [
|
|
29
|
+
'Jan',
|
|
30
|
+
'Feb',
|
|
31
|
+
'Mar',
|
|
32
|
+
'Apr',
|
|
33
|
+
'May',
|
|
34
|
+
'Jun',
|
|
35
|
+
'Jul',
|
|
36
|
+
'Aug',
|
|
37
|
+
'Sep',
|
|
38
|
+
'Oct',
|
|
39
|
+
'Nov',
|
|
40
|
+
'Dec',
|
|
41
|
+
];
|
|
42
|
+
const month = monthNames[date.getMonth()];
|
|
43
|
+
const year = date.getFullYear();
|
|
44
|
+
let hours = date.getHours();
|
|
45
|
+
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
46
|
+
hours = hours % 12 || 12;
|
|
47
|
+
const formattedHours = String(hours).padStart(2, '0');
|
|
48
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
49
|
+
return `${day} ${month} ${year}, ${formattedHours}:${minutes} ${ampm}`;
|
|
50
|
+
}
|
|
23
51
|
}
|
|
24
52
|
exports.A11yTimeUtils = A11yTimeUtils;
|