snap-ally 0.0.2 β 0.1.0-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 +44 -43
- package/dist/A11yHtmlRenderer.d.ts +3 -3
- package/dist/A11yHtmlRenderer.js +31 -18
- package/dist/A11yReportAssets.d.ts +1 -1
- package/dist/A11yReportAssets.js +20 -12
- package/dist/A11yScanner.d.ts +2 -0
- package/dist/A11yScanner.js +63 -14
- package/dist/A11yTimeUtils.d.ts +4 -0
- package/dist/A11yTimeUtils.js +15 -0
- package/dist/SnapAllyReporter.d.ts +1 -0
- package/dist/SnapAllyReporter.js +47 -23
- package/dist/models/index.d.ts +11 -1
- package/dist/templates/accessibility-report.html +205 -1324
- package/dist/templates/execution-summary.html +155 -644
- package/dist/templates/global-report-styles.css +1536 -0
- package/dist/templates/report-app.js +857 -0
- package/dist/templates/test-execution-report.html +151 -536
- package/package.json +6 -7
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# snap-ally
|
|
1
|
+
# snap-ally πΈβΏ
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/snap-ally)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -7,17 +7,13 @@ A powerful, developer-friendly Playwright reporter for **Accessibility testing**
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## πΊ Demo
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
<video src="video.webm" width="800" controls aria-label="Snap-Ally accessibility reporter demonstration video showing HTML reports and visual overlays">
|
|
14
|
-
Your browser does not support the video tag. You can <a href="video.webm">download the video</a> to view it.
|
|
15
|
-
</video>
|
|
16
|
-
</div>
|
|
12
|
+
**[βΆοΈ Watch the Demo Video](https://www.loom.com/share/853c04f1f76242a699e8f82e54733007)**
|
|
17
13
|
|
|
18
14
|
---
|
|
19
15
|
|
|
20
|
-
##
|
|
16
|
+
## β¨ Features
|
|
21
17
|
|
|
22
18
|
- **Beautiful HTML Reporting**: Comprehensive summary and detail pages.
|
|
23
19
|
- **Visual Overlays**: Highlights violations directly on the page in screenshots.
|
|
@@ -28,7 +24,7 @@ A powerful, developer-friendly Playwright reporter for **Accessibility testing**
|
|
|
28
24
|
|
|
29
25
|
---
|
|
30
26
|
|
|
31
|
-
##
|
|
27
|
+
## π Installation
|
|
32
28
|
|
|
33
29
|
```bash
|
|
34
30
|
npm install snap-ally --save-dev
|
|
@@ -36,30 +32,33 @@ npm install snap-ally --save-dev
|
|
|
36
32
|
|
|
37
33
|
---
|
|
38
34
|
|
|
39
|
-
##
|
|
35
|
+
## π οΈ Setup
|
|
40
36
|
|
|
41
37
|
Add `snap-ally` to your `playwright.config.ts`:
|
|
42
38
|
|
|
43
39
|
```typescript
|
|
44
|
-
import { defineConfig } from
|
|
40
|
+
import { defineConfig } from "@playwright/test";
|
|
45
41
|
|
|
46
42
|
export default defineConfig({
|
|
47
43
|
reporter: [
|
|
48
|
-
[
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
},
|
|
56
60
|
},
|
|
57
|
-
|
|
58
|
-
ado: {
|
|
59
|
-
organization: 'your-org',
|
|
60
|
-
project: 'your-project'
|
|
61
|
-
}
|
|
62
|
-
}]
|
|
61
|
+
],
|
|
63
62
|
],
|
|
64
63
|
});
|
|
65
64
|
```
|
|
@@ -71,23 +70,24 @@ export default defineConfig({
|
|
|
71
70
|
Import and use `scanA11y` within your Playwright tests:
|
|
72
71
|
|
|
73
72
|
```typescript
|
|
74
|
-
import { test } from
|
|
75
|
-
import { scanA11y } from
|
|
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");
|
|
76
78
|
|
|
77
|
-
test('verify page accessibility', async ({ page }, testInfo) => {
|
|
78
|
-
await page.goto('https://example.com');
|
|
79
|
-
|
|
80
79
|
// Basic scan
|
|
81
80
|
await scanA11y(page, testInfo);
|
|
82
81
|
|
|
83
82
|
// Advanced scan with configuration
|
|
84
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'],
|
|
85
88
|
rules: {
|
|
86
|
-
'color-contrast': { enabled: false },
|
|
87
|
-
}
|
|
88
|
-
tags: ['wcag2a', 'wcag2aa'], // Focus on specific WCAG levels
|
|
89
|
-
verbose: true,
|
|
90
|
-
pageKey: 'Homepage' // Custom name for the report file
|
|
89
|
+
'color-contrast': { enabled: false },
|
|
90
|
+
}
|
|
91
91
|
});
|
|
92
92
|
});
|
|
93
93
|
```
|
|
@@ -98,20 +98,21 @@ test('verify page accessibility', async ({ page }, testInfo) => {
|
|
|
98
98
|
|
|
99
99
|
### Reporter Options (in `playwright.config.ts`)
|
|
100
100
|
|
|
101
|
-
| Option
|
|
102
|
-
|
|
|
103
|
-
| `outputFolder`
|
|
104
|
-
| `colors`
|
|
105
|
-
| `ado`
|
|
106
|
-
| `ado.organization` | `string` | Your Azure DevOps organization name.
|
|
107
|
-
| `ado.project`
|
|
101
|
+
| Option | Type | Description |
|
|
102
|
+
| ------------------ | -------- | --------------------------------------------------------------- |
|
|
103
|
+
| `outputFolder` | `string` | Where to save the reports. Defaults to `steps-report`. |
|
|
104
|
+
| `colors` | `object` | Customize severity colors (critical, serious, moderate, minor). |
|
|
105
|
+
| `ado` | `object` | Azure DevOps configuration for deep linking. |
|
|
106
|
+
| `ado.organization` | `string` | Your Azure DevOps organization name. |
|
|
107
|
+
| `ado.project` | `string` | Your Azure DevOps project name. |
|
|
108
108
|
|
|
109
109
|
### `scanA11y` Options
|
|
110
110
|
|
|
111
111
|
| Option | Type | Description |
|
|
112
112
|
| --- | --- | --- |
|
|
113
113
|
| `include` | `string` | CSS selector to limit the scan to a specific element. |
|
|
114
|
-
| `verbose` | `boolean` |
|
|
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`. |
|
|
115
116
|
| `rules` | `object` | Axe-core rule configuration. |
|
|
116
117
|
| `tags` | `string[]` | List of Axe-core tags to run (e.g., `['wcag2aa']`). |
|
|
117
118
|
| `pageKey` | `string` | Custom identifier for the report file name. |
|
|
@@ -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.
|
|
@@ -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,22 +57,24 @@ 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
|
}
|
|
@@ -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,13 +118,15 @@ class A11yReportAssets {
|
|
|
116
118
|
*/
|
|
117
119
|
copyScreenshots(result, destFolder) {
|
|
118
120
|
return result.attachments
|
|
119
|
-
.filter(a => a.name === 'screenshot')
|
|
121
|
+
.filter(a => a.name === 'screenshot' || (a.contentType || '').startsWith('image/'))
|
|
120
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' ? `screenshot-${timestamp}.png` : (a.name.endsWith('.png') ? a.name : `${a.name}.png`);
|
|
129
|
+
return this.writeBuffer(destFolder, name, a.body);
|
|
126
130
|
}
|
|
127
131
|
return '';
|
|
128
132
|
})
|
|
@@ -133,14 +137,15 @@ class A11yReportAssets {
|
|
|
133
137
|
*/
|
|
134
138
|
copyPngAttachments(result, destFolder) {
|
|
135
139
|
return result.attachments
|
|
136
|
-
.filter(a => a.name.endsWith('.png') && a.name !== 'screenshot')
|
|
140
|
+
.filter(a => (a.name.endsWith('.png') || (a.contentType || '') === 'image/png') && a.name !== 'screenshot')
|
|
137
141
|
.map(a => {
|
|
138
142
|
let name = '';
|
|
139
143
|
if (a.path) {
|
|
140
144
|
name = this.copyToFolder(destFolder, a.path, a.name);
|
|
141
145
|
}
|
|
142
146
|
else if (a.body) {
|
|
143
|
-
|
|
147
|
+
const safeName = a.name.endsWith('.png') ? a.name : `${a.name}.png`;
|
|
148
|
+
name = this.writeBuffer(destFolder, safeName, a.body);
|
|
144
149
|
}
|
|
145
150
|
return name ? { path: name, name: a.name } : null;
|
|
146
151
|
})
|
|
@@ -152,7 +157,10 @@ class A11yReportAssets {
|
|
|
152
157
|
copyAllOtherAttachments(result, destFolder) {
|
|
153
158
|
const excludedNames = ['screenshot', 'video', 'A11y'];
|
|
154
159
|
return result.attachments
|
|
155
|
-
.filter(a => !excludedNames.includes(a.name) &&
|
|
160
|
+
.filter(a => !excludedNames.includes(a.name) &&
|
|
161
|
+
!a.name.endsWith('.png') &&
|
|
162
|
+
!(a.contentType || '').startsWith('image/') &&
|
|
163
|
+
!(a.contentType || '').startsWith('video/'))
|
|
156
164
|
.map(a => {
|
|
157
165
|
let name = '';
|
|
158
166
|
if (a.path) {
|
package/dist/A11yScanner.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface A11yScannerOptions {
|
|
|
16
16
|
tags?: string[];
|
|
17
17
|
/** Any other Axe-core options to pass to the builder. */
|
|
18
18
|
axeOptions?: Record<string, unknown>;
|
|
19
|
+
/** Custom identifier for the report file name. */
|
|
20
|
+
pageKey?: string;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* Performs an accessibility audit using Axe and Lighthouse.
|
package/dist/A11yScanner.js
CHANGED
|
@@ -9,13 +9,39 @@ 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");
|
|
13
|
+
/**
|
|
14
|
+
* Sanitizes a string to be safe for use in file paths and prevents path traversal attacks.
|
|
15
|
+
* Removes or replaces dangerous characters and path separators.
|
|
16
|
+
*/
|
|
17
|
+
function sanitizePageKey(input) {
|
|
18
|
+
return input
|
|
19
|
+
// Remove protocol
|
|
20
|
+
.replace(/^https?:\/\//, '')
|
|
21
|
+
// Remove or replace path separators and dangerous characters
|
|
22
|
+
.replace(/[\/\\:*?"<>|]/g, '-')
|
|
23
|
+
// Remove any remaining path traversal attempts
|
|
24
|
+
.replace(/\.\./g, '')
|
|
25
|
+
// Replace multiple consecutive dashes with a single dash
|
|
26
|
+
.replace(/-+/g, '-')
|
|
27
|
+
// Remove leading/trailing dashes
|
|
28
|
+
.replace(/^-+|-+$/g, '')
|
|
29
|
+
// Convert to lowercase for consistency
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
// Limit length to prevent filesystem issues
|
|
32
|
+
.substring(0, 200);
|
|
33
|
+
}
|
|
12
34
|
/**
|
|
13
35
|
* Performs an accessibility audit using Axe and Lighthouse.
|
|
14
36
|
*/
|
|
15
37
|
async function scanA11y(page, testInfo, options = {}) {
|
|
16
|
-
var _a;
|
|
17
|
-
const
|
|
18
|
-
const
|
|
38
|
+
var _a, _b;
|
|
39
|
+
const showTerminal = (_a = options.verbose) !== null && _a !== void 0 ? _a : true;
|
|
40
|
+
const showBrowser = (_b = options.consoleLog) !== null && _b !== void 0 ? _b : true;
|
|
41
|
+
// Sanitize pageKey to prevent path traversal attacks
|
|
42
|
+
const rawPageKey = options.pageKey || page.url();
|
|
43
|
+
const pageKey = sanitizePageKey(rawPageKey);
|
|
44
|
+
const overlay = new A11yAuditOverlay_1.A11yAuditOverlay(page, pageKey);
|
|
19
45
|
// Configure Axe
|
|
20
46
|
let axeBuilder = new playwright_1.default({ page });
|
|
21
47
|
const target = options.include || options.box;
|
|
@@ -37,13 +63,34 @@ async function scanA11y(page, testInfo, options = {}) {
|
|
|
37
63
|
if (options.axeOptions) {
|
|
38
64
|
axeBuilder = axeBuilder.options(options.axeOptions);
|
|
39
65
|
}
|
|
40
|
-
|
|
66
|
+
let axeResults;
|
|
67
|
+
try {
|
|
68
|
+
axeResults = await axeBuilder.analyze();
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error.message && (error.message.includes('Test ended') || error.message.includes('Target page, context or browser has been closed'))) {
|
|
72
|
+
console.warn(`[SnapAlly] Accessibility scan skipped: ${error.message}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
41
77
|
const violationCount = axeResults.violations.length;
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
78
|
+
if ((showTerminal || showBrowser) && violationCount > 0) {
|
|
79
|
+
const mainMsg = `[A11yScanner] Violations found: ${violationCount}`;
|
|
80
|
+
// Prepare all detail messages
|
|
81
|
+
const detailMessages = axeResults.violations.map((v, i) => ` ${i + 1}. ${v.id} [${v.impact}] - ${v.help}`);
|
|
82
|
+
// Log to terminal
|
|
83
|
+
if (showTerminal) {
|
|
84
|
+
console.log(`\n${mainMsg}`);
|
|
85
|
+
detailMessages.forEach(msg => console.log(msg));
|
|
86
|
+
}
|
|
87
|
+
// Batch log to Browser Console in a single evaluate call
|
|
88
|
+
if (showBrowser) {
|
|
89
|
+
await page.evaluate(([mainMsg, details]) => {
|
|
90
|
+
console.log(`%c ${mainMsg}`, 'color: #ea580c; font-weight: bold; font-size: 12px;');
|
|
91
|
+
details.forEach((msg) => console.log(msg));
|
|
92
|
+
}, [mainMsg, detailMessages]);
|
|
93
|
+
}
|
|
47
94
|
}
|
|
48
95
|
// Fail the test if violations found (softly)
|
|
49
96
|
test_1.expect.soft(violationCount, `Accessibility audit failed with ${violationCount} violations.`).toBe(0);
|
|
@@ -67,9 +114,9 @@ async function scanA11y(page, testInfo, options = {}) {
|
|
|
67
114
|
await overlay.showViolationOverlay({ id: violation.id, help: violation.help }, severityColor);
|
|
68
115
|
if (await locator.isVisible()) {
|
|
69
116
|
await overlay.highlightElement(elementSelector, severityColor);
|
|
70
|
-
// Allow time for
|
|
117
|
+
// Allow a small time for overlay highlight to be visible in video
|
|
71
118
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
72
|
-
await page.waitForTimeout(
|
|
119
|
+
await page.waitForTimeout(100);
|
|
73
120
|
const screenshotName = `a11y-${violation.id}-${errorIdx++}.png`;
|
|
74
121
|
const buffer = await overlay.captureAndAttachScreenshot(screenshotName, testInfo);
|
|
75
122
|
// Capture execution steps for context
|
|
@@ -106,16 +153,18 @@ async function scanA11y(page, testInfo, options = {}) {
|
|
|
106
153
|
}
|
|
107
154
|
// Prepare data for the reporter
|
|
108
155
|
const reportData = {
|
|
109
|
-
pageKey
|
|
156
|
+
pageKey,
|
|
157
|
+
pageUrl: page.url(),
|
|
110
158
|
accessibilityScore: 0, // No longer used, derivation from Lighthouse removed
|
|
111
|
-
errors,
|
|
159
|
+
a11yErrors: errors,
|
|
112
160
|
video: 'a11y-scan-video.webm', // Reference name for reporter
|
|
113
161
|
criticalColor: models_1.Severity.critical,
|
|
114
162
|
seriousColor: models_1.Severity.serious,
|
|
115
163
|
moderateColor: models_1.Severity.moderate,
|
|
116
164
|
minorColor: models_1.Severity.minor,
|
|
117
165
|
adoOrganization: process.env.ADO_ORGANIZATION || '',
|
|
118
|
-
adoProject: process.env.ADO_PROJECT || ''
|
|
166
|
+
adoProject: process.env.ADO_PROJECT || '',
|
|
167
|
+
timestamp: A11yTimeUtils_1.A11yTimeUtils.formatDate(new Date())
|
|
119
168
|
};
|
|
120
169
|
await overlay.addTestAttachment(testInfo, 'A11y', JSON.stringify(reportData));
|
|
121
170
|
await overlay.hideViolationOverlay();
|
package/dist/A11yTimeUtils.d.ts
CHANGED
package/dist/A11yTimeUtils.js
CHANGED
|
@@ -20,5 +20,20 @@ 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 = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
29
|
+
const month = monthNames[date.getMonth()];
|
|
30
|
+
const year = date.getFullYear();
|
|
31
|
+
let hours = date.getHours();
|
|
32
|
+
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
33
|
+
hours = hours % 12 || 12;
|
|
34
|
+
const formattedHours = String(hours).padStart(2, '0');
|
|
35
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
36
|
+
return `${day} ${month} ${year}, ${formattedHours}:${minutes} ${ampm}`;
|
|
37
|
+
}
|
|
23
38
|
}
|
|
24
39
|
exports.A11yTimeUtils = A11yTimeUtils;
|