snap-ally 0.2.7-beta → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/A11yReportAssets.d.ts +5 -12
- package/dist/A11yReportAssets.js +16 -82
- package/dist/A11yScanner.d.ts +2 -21
- package/dist/A11yScanner.js +16 -22
- package/dist/A11yTimeUtils.js +11 -23
- package/dist/A11yVisualReporter.d.ts +50 -0
- package/dist/A11yVisualReporter.js +188 -0
- package/dist/AccessibilityReporterOptions.d.ts +24 -0
- package/dist/AccessibilityReporterOptions.js +5 -0
- package/dist/ResolvedColors.d.ts +15 -0
- package/dist/ResolvedColors.js +20 -0
- package/dist/SnapAllyReporter.d.ts +12 -72
- package/dist/SnapAllyReporter.js +218 -329
- package/dist/core/A11yHtmlRenderer.d.ts +17 -0
- package/dist/core/A11yHtmlRenderer.js +118 -0
- package/dist/core/A11yReportAssets.d.ts +30 -0
- package/dist/core/A11yReportAssets.js +127 -0
- package/dist/core/A11yScanner.d.ts +8 -0
- package/dist/core/A11yScanner.js +178 -0
- package/dist/core/A11yVisualReporter.d.ts +50 -0
- package/dist/core/A11yVisualReporter.js +188 -0
- package/dist/core/HtmlRenderer.d.ts +14 -0
- package/dist/core/HtmlRenderer.js +106 -0
- package/dist/core/ReportAssets.d.ts +29 -0
- package/dist/core/ReportAssets.js +126 -0
- package/dist/core/Scanner.d.ts +7 -0
- package/dist/core/Scanner.js +162 -0
- package/dist/core/VisualReporter.d.ts +54 -0
- package/dist/core/VisualReporter.js +192 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +13 -12
- package/dist/models/A11yDataSource.d.ts +15 -0
- package/dist/models/A11yDataSource.js +2 -0
- package/dist/models/A11yError.d.ts +34 -0
- package/dist/models/A11yError.js +11 -0
- package/dist/models/A11yScannerOptions.d.ts +24 -0
- package/dist/models/A11yScannerOptions.js +2 -0
- package/dist/models/AccessibilityReporterOptions.d.ts +24 -0
- package/dist/models/AccessibilityReporterOptions.js +5 -0
- package/dist/models/DataSource.d.ts +15 -0
- package/dist/models/DataSource.js +2 -0
- package/dist/models/ImagePath.d.ts +5 -0
- package/dist/models/ImagePath.js +3 -0
- package/dist/models/ReportData.d.ts +24 -0
- package/dist/models/ReportData.js +2 -0
- package/dist/models/ReporterOptions.d.ts +24 -0
- package/dist/models/ReporterOptions.js +5 -0
- package/dist/models/ResolvedColors.d.ts +16 -0
- package/dist/models/ResolvedColors.js +24 -0
- package/dist/models/ScannerOptions.d.ts +30 -0
- package/dist/models/ScannerOptions.js +2 -0
- package/dist/models/Severity.d.ts +7 -0
- package/dist/models/Severity.js +11 -0
- package/dist/models/Target.d.ts +10 -0
- package/dist/models/Target.js +3 -0
- package/dist/models/TestResults.d.ts +41 -0
- package/dist/models/TestResults.js +2 -0
- package/dist/models/TestStatusIcon.d.ts +8 -0
- package/dist/models/TestStatusIcon.js +12 -0
- package/dist/models/TestSummary.d.ts +34 -0
- package/dist/models/TestSummary.js +2 -0
- package/dist/models/Violation.d.ts +13 -0
- package/dist/models/Violation.js +2 -0
- package/dist/models/index.d.ts +12 -113
- package/dist/models/index.js +26 -16
- package/dist/templates/accessibility-report.html +62 -95
- package/dist/templates/execution-summary.html +37 -103
- package/dist/templates/global-report-styles.css +400 -9
- package/dist/templates/report-app.js +170 -72
- package/dist/templates/test-execution-report.html +84 -121
- package/dist/utils/A11yTimeUtils.d.ts +13 -0
- package/dist/utils/A11yTimeUtils.js +40 -0
- package/dist/utils/TimeUtils.d.ts +13 -0
- package/dist/utils/TimeUtils.js +39 -0
- package/package.json +2 -2
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.A11yHtmlRenderer = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
/**
|
|
40
|
+
* Handles the rendering of HTML reports using static templates and JSON data injection.
|
|
41
|
+
*/
|
|
42
|
+
class A11yHtmlRenderer {
|
|
43
|
+
/**
|
|
44
|
+
* Renders a static HTML template by copying it and generating the accompanied data payload.
|
|
45
|
+
* @param templateName The template file name in the templates folder.
|
|
46
|
+
* @param data The data object to pass to the client-side JS app.
|
|
47
|
+
* @param outputFolder The folder where the rendered file will be saved.
|
|
48
|
+
* @param outputFileName The full path of the output file.
|
|
49
|
+
*/
|
|
50
|
+
async render(templateName, data, outputFolder, outputFileName) {
|
|
51
|
+
// Resolve path relative to this file (dist/core/A11yHtmlRenderer.js)
|
|
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)) {
|
|
57
|
+
throw new Error(`[A11yHtmlRenderer] Template not found: ${templatePath}`);
|
|
58
|
+
}
|
|
59
|
+
if (!fs.existsSync(outputFolder)) {
|
|
60
|
+
fs.mkdirSync(outputFolder, { recursive: true });
|
|
61
|
+
}
|
|
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
|
|
71
|
+
const outputDir = path.dirname(outputFileName);
|
|
72
|
+
const dataJsPath = path.join(outputDir, dataJsName);
|
|
73
|
+
const jsContent = `window.snapAllyData = ${JSON.stringify(data)};`;
|
|
74
|
+
fs.writeFileSync(dataJsPath, jsContent, 'utf8');
|
|
75
|
+
// 3. Copy the global CSS and JS rendering engine next to the HTML file
|
|
76
|
+
const outCssPath = path.join(outputDir, 'global-report-styles.css');
|
|
77
|
+
const outJsPath = path.join(outputDir, 'report-app.js');
|
|
78
|
+
try {
|
|
79
|
+
if (fs.existsSync(cssPath))
|
|
80
|
+
fs.copyFileSync(cssPath, outCssPath);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
console.error('Error copying CSS:', e);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
if (fs.existsSync(jsPath))
|
|
87
|
+
fs.copyFileSync(jsPath, outJsPath);
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
console.error('Error copying JS:', e);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Converts ANSI color codes to HTML spans for nicer error display.
|
|
95
|
+
*/
|
|
96
|
+
ansiToHtml(text) {
|
|
97
|
+
const map = {
|
|
98
|
+
'\u001b[30m': '<span style="color:black">',
|
|
99
|
+
'\u001b[31m': '<span style="color:red">',
|
|
100
|
+
'\u001b[32m': '<span style="color:green">',
|
|
101
|
+
'\u001b[33m': '<span style="color:yellow">',
|
|
102
|
+
'\u001b[34m': '<span style="color:blue">',
|
|
103
|
+
'\u001b[35m': '<span style="color:magenta">',
|
|
104
|
+
'\u001b[36m': '<span style="color:cyan">',
|
|
105
|
+
'\u001b[37m': '<span style="color:white">',
|
|
106
|
+
'\u001b[0m': '</span>',
|
|
107
|
+
'\u001b[2m': '<span style="opacity:0.5">',
|
|
108
|
+
'\u001b[22m': '</span>',
|
|
109
|
+
'\u001b[39m': '</span>',
|
|
110
|
+
};
|
|
111
|
+
let result = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
112
|
+
for (const [code, tag] of Object.entries(map)) {
|
|
113
|
+
result = result.split(code).join(tag);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
exports.A11yHtmlRenderer = A11yHtmlRenderer;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { TestResult } from '@playwright/test/reporter';
|
|
2
|
+
/**
|
|
3
|
+
* Utilities for managing and copying report assets like videos and screenshots.
|
|
4
|
+
*/
|
|
5
|
+
export declare class A11yReportAssets {
|
|
6
|
+
/**
|
|
7
|
+
* Copies a file from source to a destination folder.
|
|
8
|
+
*/
|
|
9
|
+
copyToFolder(destFolder: string, srcPath: string, fileName?: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Copies all video attachments to the report folder for portability.
|
|
12
|
+
* @returns An array of filenames written to the destination folder.
|
|
13
|
+
*/
|
|
14
|
+
copyVideos(result: TestResult, destFolder: string): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Copies all screenshots found in the test attachments.
|
|
17
|
+
*/
|
|
18
|
+
copyScreenshots(result: TestResult, destFolder: string): string[];
|
|
19
|
+
/**
|
|
20
|
+
* Copies all other attachments (traces, logs, etc.) to the report folder.
|
|
21
|
+
*/
|
|
22
|
+
copyAllOtherAttachments(result: TestResult, destFolder: string): {
|
|
23
|
+
path: string;
|
|
24
|
+
name: string;
|
|
25
|
+
}[];
|
|
26
|
+
/**
|
|
27
|
+
* Persists an in-memory buffer to a file in the destination folder.
|
|
28
|
+
*/
|
|
29
|
+
saveBuffer(destFolder: string, fileName: string, buffer: Buffer): string;
|
|
30
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.A11yReportAssets = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
/**
|
|
40
|
+
* Utilities for managing and copying report assets like videos and screenshots.
|
|
41
|
+
*/
|
|
42
|
+
class A11yReportAssets {
|
|
43
|
+
/**
|
|
44
|
+
* Copies a file from source to a destination folder.
|
|
45
|
+
*/
|
|
46
|
+
copyToFolder(destFolder, srcPath, fileName) {
|
|
47
|
+
if (!srcPath || !fs.existsSync(srcPath)) {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
const name = fileName || path.basename(srcPath);
|
|
51
|
+
const destFile = path.join(destFolder, name);
|
|
52
|
+
if (!fs.existsSync(destFolder)) {
|
|
53
|
+
fs.mkdirSync(destFolder, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
fs.copyFileSync(srcPath, destFile);
|
|
56
|
+
return name;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Copies all video attachments to the report folder for portability.
|
|
60
|
+
* @returns An array of filenames written to the destination folder.
|
|
61
|
+
*/
|
|
62
|
+
copyVideos(result, destFolder) {
|
|
63
|
+
return result.attachments
|
|
64
|
+
.filter((a) => (a.name === 'video' || (a.contentType || '').startsWith('video/')) && a.path)
|
|
65
|
+
.map((attachment) => this.copyToFolder(destFolder, attachment.path))
|
|
66
|
+
.filter((p) => !!p);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Copies all screenshots found in the test attachments.
|
|
70
|
+
*/
|
|
71
|
+
copyScreenshots(result, destFolder) {
|
|
72
|
+
return result.attachments
|
|
73
|
+
.filter((a) => a.name === 'screenshot' ||
|
|
74
|
+
a.name.endsWith('.png') ||
|
|
75
|
+
(a.contentType || '').startsWith('image/'))
|
|
76
|
+
.map((a) => {
|
|
77
|
+
if (a.path) {
|
|
78
|
+
return this.copyToFolder(destFolder, a.path, a.name !== 'screenshot' ? a.name : undefined);
|
|
79
|
+
}
|
|
80
|
+
else if (a.body) {
|
|
81
|
+
const timestamp = Date.now();
|
|
82
|
+
const name = a.name === 'screenshot'
|
|
83
|
+
? `screenshot-${timestamp}.png`
|
|
84
|
+
: a.name.endsWith('.png')
|
|
85
|
+
? a.name
|
|
86
|
+
: `${a.name}.png`;
|
|
87
|
+
return this.saveBuffer(destFolder, name, a.body);
|
|
88
|
+
}
|
|
89
|
+
return '';
|
|
90
|
+
})
|
|
91
|
+
.filter((path) => path !== '');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Copies all other attachments (traces, logs, etc.) to the report folder.
|
|
95
|
+
*/
|
|
96
|
+
copyAllOtherAttachments(result, destFolder) {
|
|
97
|
+
const excludedNames = ['screenshot', 'video', 'A11y'];
|
|
98
|
+
return result.attachments
|
|
99
|
+
.filter((a) => !excludedNames.includes(a.name) &&
|
|
100
|
+
!a.name.toLowerCase().endsWith('.png') &&
|
|
101
|
+
!(a.contentType || '').startsWith('image/') &&
|
|
102
|
+
!(a.contentType || '').startsWith('video/'))
|
|
103
|
+
.map((a) => {
|
|
104
|
+
let name = '';
|
|
105
|
+
if (a.path) {
|
|
106
|
+
name = this.copyToFolder(destFolder, a.path, a.name);
|
|
107
|
+
}
|
|
108
|
+
else if (a.body) {
|
|
109
|
+
name = this.saveBuffer(destFolder, a.name, a.body);
|
|
110
|
+
}
|
|
111
|
+
return name ? { path: name, name: a.name } : null;
|
|
112
|
+
})
|
|
113
|
+
.filter((item) => item !== null);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Persists an in-memory buffer to a file in the destination folder.
|
|
117
|
+
*/
|
|
118
|
+
saveBuffer(destFolder, fileName, buffer) {
|
|
119
|
+
if (!fs.existsSync(destFolder)) {
|
|
120
|
+
fs.mkdirSync(destFolder, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
const destFile = path.join(destFolder, fileName);
|
|
123
|
+
fs.writeFileSync(destFile, buffer);
|
|
124
|
+
return fileName;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.A11yReportAssets = A11yReportAssets;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Page, TestInfo } from '@playwright/test';
|
|
2
|
+
import { A11yScannerOptions } from '../models';
|
|
3
|
+
/**
|
|
4
|
+
* Performs an accessibility audit using Axe and Lighthouse.
|
|
5
|
+
*/
|
|
6
|
+
export declare function scanA11y(page: Page, testInfo: TestInfo, options?: A11yScannerOptions): Promise<void>;
|
|
7
|
+
/** Alias for backward compatibility */
|
|
8
|
+
export declare const checkAccessibility: typeof scanA11y;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.checkAccessibility = void 0;
|
|
7
|
+
exports.scanA11y = scanA11y;
|
|
8
|
+
const playwright_1 = __importDefault(require("@axe-core/playwright"));
|
|
9
|
+
const A11yVisualReporter_1 = require("./A11yVisualReporter");
|
|
10
|
+
const models_1 = require("../models");
|
|
11
|
+
const A11yTimeUtils_1 = require("../utils/A11yTimeUtils");
|
|
12
|
+
/**
|
|
13
|
+
* Sanitizes a string to be safe for use in file paths and prevents path traversal attacks.
|
|
14
|
+
* Removes or replaces dangerous characters and path separators.
|
|
15
|
+
*/
|
|
16
|
+
function sanitizePageKey(input) {
|
|
17
|
+
return (input
|
|
18
|
+
// Remove protocol
|
|
19
|
+
.replace(/^https?:\/\//, '')
|
|
20
|
+
// Remove or replace path separators and dangerous characters
|
|
21
|
+
.replace(/[\/\\:*?"<>|]/g, '-')
|
|
22
|
+
// Remove any remaining path traversal attempts
|
|
23
|
+
.replace(/\.\./g, '')
|
|
24
|
+
// Replace multiple consecutive dashes with a single dash
|
|
25
|
+
.replace(/-+/g, '-')
|
|
26
|
+
// Remove leading/trailing dashes
|
|
27
|
+
.replace(/^-+|-+$/g, '')
|
|
28
|
+
// Convert to lowercase for consistency
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
// Limit length to prevent filesystem issues
|
|
31
|
+
.substring(0, 200));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Performs an accessibility audit using Axe and Lighthouse.
|
|
35
|
+
*/
|
|
36
|
+
async function scanA11y(page, testInfo, options = {}) {
|
|
37
|
+
var _a, _b;
|
|
38
|
+
const showTerminal = (_a = options.verbose) !== null && _a !== void 0 ? _a : true;
|
|
39
|
+
const showBrowser = (_b = options.consoleLog) !== null && _b !== void 0 ? _b : true;
|
|
40
|
+
// Sanitize pageKey to prevent path traversal attacks
|
|
41
|
+
const rawPageKey = options.pageKey || page.url();
|
|
42
|
+
const pageKey = sanitizePageKey(rawPageKey);
|
|
43
|
+
const overlay = new A11yVisualReporter_1.A11yVisualReporter(page);
|
|
44
|
+
// Configure Axe
|
|
45
|
+
let axeBuilder = new playwright_1.default({ page });
|
|
46
|
+
const target = options.include || options.box;
|
|
47
|
+
if (target) {
|
|
48
|
+
if (typeof target === 'string') {
|
|
49
|
+
axeBuilder = axeBuilder.include(target);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// AxeBuilder for playwright also supports locators/elements in include
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
axeBuilder = axeBuilder.include(target);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (options.rules) {
|
|
58
|
+
axeBuilder = axeBuilder.options({ rules: options.rules });
|
|
59
|
+
}
|
|
60
|
+
if (options.tags) {
|
|
61
|
+
axeBuilder = axeBuilder.withTags(options.tags);
|
|
62
|
+
}
|
|
63
|
+
if (options.axeOptions) {
|
|
64
|
+
axeBuilder = axeBuilder.options(options.axeOptions);
|
|
65
|
+
}
|
|
66
|
+
let axeResults;
|
|
67
|
+
try {
|
|
68
|
+
axeResults = await axeBuilder.analyze();
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error instanceof Error &&
|
|
72
|
+
(error.message.includes('Test ended') ||
|
|
73
|
+
error.message.includes('Target page, context or browser has been closed'))) {
|
|
74
|
+
console.warn(`[SnapAlly] Accessibility scan skipped: ${error.message}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
const violationCount = axeResults.violations.length;
|
|
80
|
+
if ((showTerminal || showBrowser) && violationCount > 0) {
|
|
81
|
+
const mainMsg = `[A11yScanner] Violations found: ${violationCount}`;
|
|
82
|
+
// Prepare all detail messages
|
|
83
|
+
const detailMessages = axeResults.violations.map((v, i) => ` ${i + 1}. ${v.id} [${v.impact}] - ${v.help}`);
|
|
84
|
+
// Log to terminal
|
|
85
|
+
if (showTerminal) {
|
|
86
|
+
console.log(`\n${mainMsg}`);
|
|
87
|
+
detailMessages.forEach((msg) => console.log(msg));
|
|
88
|
+
}
|
|
89
|
+
// Batch log to Browser Console in a single evaluate call
|
|
90
|
+
if (showBrowser) {
|
|
91
|
+
await page.evaluate(([mainMsg, details, color]) => {
|
|
92
|
+
console.log(`%c ${mainMsg}`, `color: ${color}; font-weight: bold; font-size: 12px;`);
|
|
93
|
+
details.forEach((msg) => console.log(msg));
|
|
94
|
+
}, [mainMsg, detailMessages, models_1.DEFAULT_COLORS.serious]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Fail the test if violations found (softly)
|
|
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
|
|
102
|
+
.soft(violationCount, `Accessibility audit failed with ${violationCount} violations.`)
|
|
103
|
+
.toBe(0);
|
|
104
|
+
// Run Axe Audit
|
|
105
|
+
const errors = [];
|
|
106
|
+
// Process violations for the report
|
|
107
|
+
for (const violation of axeResults.violations) {
|
|
108
|
+
let errorIdx = 0;
|
|
109
|
+
const targets = [];
|
|
110
|
+
const severityColor = (0, models_1.getSeverityColor)(violation.impact);
|
|
111
|
+
for (const node of violation.nodes) {
|
|
112
|
+
for (const selector of node.target) {
|
|
113
|
+
const elementSelector = selector.toString();
|
|
114
|
+
const locator = page.locator(elementSelector);
|
|
115
|
+
await overlay.showBanner({ id: violation.id, help: violation.help }, severityColor);
|
|
116
|
+
if (await locator.isVisible()) {
|
|
117
|
+
await overlay.highlightElement(elementSelector, severityColor);
|
|
118
|
+
// Allow a small time for overlay highlight to be visible in video
|
|
119
|
+
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
120
|
+
await page.waitForTimeout(100);
|
|
121
|
+
const screenshotName = `a11y-${violation.id}-${errorIdx++}.png`;
|
|
122
|
+
const buffer = await overlay.captureScreenshot(screenshotName, testInfo);
|
|
123
|
+
// Capture execution steps for context
|
|
124
|
+
const excluded = new Set([
|
|
125
|
+
'Pre Condition',
|
|
126
|
+
'Post Condition',
|
|
127
|
+
'Description',
|
|
128
|
+
'A11y',
|
|
129
|
+
]);
|
|
130
|
+
const contextSteps = (testInfo.annotations || [])
|
|
131
|
+
.filter((a) => !excluded.has(a.type))
|
|
132
|
+
.map((a) => a.description || '');
|
|
133
|
+
const nodeHtml = node.html || '';
|
|
134
|
+
const friendlySnippet = elementSelector; // Use full CSS selector path from Axe core
|
|
135
|
+
targets.push({
|
|
136
|
+
element: elementSelector,
|
|
137
|
+
snippet: friendlySnippet,
|
|
138
|
+
html: nodeHtml,
|
|
139
|
+
screenshot: screenshotName,
|
|
140
|
+
steps: contextSteps,
|
|
141
|
+
stepsJson: JSON.stringify(contextSteps),
|
|
142
|
+
screenshotBase64: buffer.toString('base64'),
|
|
143
|
+
});
|
|
144
|
+
await overlay.removeHighlight();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
errors.push({
|
|
149
|
+
id: violation.id,
|
|
150
|
+
description: violation.description,
|
|
151
|
+
severity: violation.impact || 'unknown',
|
|
152
|
+
helpUrl: violation.helpUrl,
|
|
153
|
+
help: violation.help,
|
|
154
|
+
guideline: violation.tags[1] || 'N/A',
|
|
155
|
+
wcagRule: violation.tags.find((t) => t.startsWith('wcag')) || violation.tags[1] || 'N/A',
|
|
156
|
+
total: targets.length || violation.nodes.length, // Fallback to node count if no screenshots
|
|
157
|
+
target: targets,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// Prepare data for the reporter
|
|
161
|
+
const reportData = {
|
|
162
|
+
pageKey,
|
|
163
|
+
pageUrl: page.url(),
|
|
164
|
+
accessibilityScore: 0, // No longer used, derivation from Lighthouse removed
|
|
165
|
+
a11yErrors: errors,
|
|
166
|
+
criticalColor: models_1.DEFAULT_COLORS.critical,
|
|
167
|
+
seriousColor: models_1.DEFAULT_COLORS.serious,
|
|
168
|
+
moderateColor: models_1.DEFAULT_COLORS.moderate,
|
|
169
|
+
minorColor: models_1.DEFAULT_COLORS.minor,
|
|
170
|
+
adoOrganization: process.env.ADO_ORGANIZATION || '',
|
|
171
|
+
adoProject: process.env.ADO_PROJECT || '',
|
|
172
|
+
timestamp: A11yTimeUtils_1.A11yTimeUtils.formatDate(new Date()),
|
|
173
|
+
};
|
|
174
|
+
await overlay.attachData(testInfo, 'A11y', JSON.stringify(reportData));
|
|
175
|
+
await overlay.clean();
|
|
176
|
+
}
|
|
177
|
+
/** Alias for backward compatibility */
|
|
178
|
+
exports.checkAccessibility = scanA11y;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Page, TestInfo } from '@playwright/test';
|
|
2
|
+
/**
|
|
3
|
+
* Manages visual feedback on the page during an accessibility scan.
|
|
4
|
+
* Handles element highlights, violation banners, and report attachments.
|
|
5
|
+
*/
|
|
6
|
+
export declare class A11yVisualReporter {
|
|
7
|
+
private readonly page;
|
|
8
|
+
private readonly rootId;
|
|
9
|
+
private static readonly BANNER_ID;
|
|
10
|
+
private static readonly HIGHLIGHT_ID;
|
|
11
|
+
constructor(page: Page);
|
|
12
|
+
/**
|
|
13
|
+
* Shows a violation banner at the bottom of the page.
|
|
14
|
+
*/
|
|
15
|
+
showBanner(violation: {
|
|
16
|
+
id: string;
|
|
17
|
+
help: string;
|
|
18
|
+
}, color: string): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Highlights an element on the page.
|
|
21
|
+
*/
|
|
22
|
+
highlightElement(selector: string, color: string): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Removes all visual feedback from the page.
|
|
25
|
+
*/
|
|
26
|
+
clean(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Removes the element highlight.
|
|
29
|
+
*/
|
|
30
|
+
removeHighlight(): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Attaches JSON data to the test report.
|
|
33
|
+
*/
|
|
34
|
+
attachData(testInfo: TestInfo, name: string, data: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Captures and attaches a screenshot to the test report.
|
|
37
|
+
*/
|
|
38
|
+
captureScreenshot(name: string, testInfo: TestInfo): Promise<Buffer>;
|
|
39
|
+
/**
|
|
40
|
+
* Injects helper functions into the page context.
|
|
41
|
+
*/
|
|
42
|
+
private ensureHelpers;
|
|
43
|
+
private safeEvaluate;
|
|
44
|
+
}
|
|
45
|
+
declare global {
|
|
46
|
+
interface Window {
|
|
47
|
+
snapAllyGetRoot: (id: string) => ShadowRoot;
|
|
48
|
+
snapAllyToAlpha: (color: string, alpha: number) => string;
|
|
49
|
+
}
|
|
50
|
+
}
|