mcp-accessibility-scanner 1.0.9 → 1.0.11
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 +111 -15
- package/build/accessibilityChecker.js +339 -107
- package/build/index.js +896 -1
- package/package.json +1 -1
package/Readme.md
CHANGED
|
@@ -2,13 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
# MCP Accessibility Scanner 🔍
|
|
4
4
|
|
|
5
|
-
A Model Context Protocol (MCP) server that provides automated web accessibility scanning using Playwright and Axe-core. This server enables LLMs to perform WCAG compliance checks,
|
|
5
|
+
A powerful Model Context Protocol (MCP) server that provides automated web accessibility scanning and browser automation using Playwright and Axe-core. This server enables LLMs to perform WCAG compliance checks, interact with web pages, manage persistent browser sessions, and generate detailed accessibility reports with visual annotations.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
### Accessibility Scanning
|
|
10
|
+
✅ Full WCAG 2.0/2.1/2.2 compliance checking (A, AA, AAA levels)
|
|
10
11
|
🖼️ Automatic screenshot capture with violation highlighting
|
|
11
|
-
📄 Detailed JSON reports with remediation guidance
|
|
12
|
+
📄 Detailed JSON reports with remediation guidance
|
|
13
|
+
🎯 Support for specific violation categories (color contrast, ARIA, forms, keyboard navigation, etc.)
|
|
14
|
+
|
|
15
|
+
### Browser Automation
|
|
16
|
+
🖱️ Click elements by CSS selector or visible text
|
|
17
|
+
⌨️ Type text into inputs by selector or label
|
|
18
|
+
🔍 Analyze pages to discover all interactive elements
|
|
19
|
+
📸 Capture screenshots after each interaction
|
|
20
|
+
|
|
21
|
+
### Session Management
|
|
22
|
+
🔄 Create persistent browser sessions for multi-step workflows
|
|
23
|
+
⏱️ Automatic session cleanup after 3 minutes of inactivity
|
|
24
|
+
🌐 Navigate between pages while maintaining session state
|
|
25
|
+
📊 Run accessibility scans within active sessions
|
|
12
26
|
|
|
13
27
|
## Installation
|
|
14
28
|
|
|
@@ -67,29 +81,111 @@ Here's the Claude Desktop configuration:
|
|
|
67
81
|
}
|
|
68
82
|
```
|
|
69
83
|
|
|
70
|
-
##
|
|
84
|
+
## Available Tools
|
|
85
|
+
|
|
86
|
+
The MCP server exposes 18 tools for accessibility scanning and browser automation:
|
|
87
|
+
|
|
88
|
+
### Accessibility Scanning
|
|
71
89
|
|
|
72
|
-
|
|
90
|
+
#### `accessibility-scan`
|
|
91
|
+
Performs a comprehensive accessibility scan on a webpage.
|
|
73
92
|
|
|
93
|
+
**Parameters:**
|
|
74
94
|
- `url`: The webpage URL to scan (required)
|
|
75
|
-
- `violationsTag`: Array of
|
|
76
|
-
- `viewport`: Optional
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
95
|
+
- `violationsTag`: Array of WCAG/violation tags to check (required)
|
|
96
|
+
- `viewport`: Optional viewport size (default: 1920x1080)
|
|
97
|
+
- `shouldRunInHeadless`: Optional headless mode control (default: true)
|
|
98
|
+
|
|
99
|
+
**Supported Violation Tags:**
|
|
100
|
+
- WCAG levels: `wcag2a`, `wcag2aa`, `wcag2aaa`, `wcag21a`, `wcag21aa`, `wcag21aaa`, `wcag22a`, `wcag22aa`, `wcag22aaa`
|
|
101
|
+
- Section 508: `section508`
|
|
102
|
+
- Categories: `cat.color` (contrast), `cat.aria`, `cat.forms`, `cat.keyboard`, `cat.language`, `cat.structure`, etc.
|
|
103
|
+
|
|
104
|
+
### Browser Automation
|
|
105
|
+
|
|
106
|
+
#### `click-element`
|
|
107
|
+
Clicks an element by CSS selector.
|
|
108
|
+
- Parameters: `url`, `selector`, `viewport`, `shouldRunInHeadless`
|
|
109
|
+
|
|
110
|
+
#### `click-element-by-text`
|
|
111
|
+
Clicks elements by their visible text content.
|
|
112
|
+
- Parameters: `url`, `text`, `elementType` (optional), `viewport`, `shouldRunInHeadless`
|
|
113
|
+
|
|
114
|
+
#### `type-text`
|
|
115
|
+
Types text into an input field by CSS selector.
|
|
116
|
+
- Parameters: `url`, `selector`, `text`, `viewport`, `shouldRunInHeadless`
|
|
117
|
+
|
|
118
|
+
#### `type-text-by-label`
|
|
119
|
+
Types text into input fields by their label text.
|
|
120
|
+
- Parameters: `url`, `labelText`, `text`, `viewport`, `shouldRunInHeadless`
|
|
121
|
+
|
|
122
|
+
#### `analyze-page`
|
|
123
|
+
Analyzes page to identify all interactive elements.
|
|
124
|
+
- Parameters: `url`, `viewport`, `shouldRunInHeadless`
|
|
125
|
+
- Returns: Lists of all buttons, links, and inputs on the page
|
|
126
|
+
|
|
127
|
+
### Session Management
|
|
128
|
+
|
|
129
|
+
#### `create-session`
|
|
130
|
+
Creates a persistent browser session for multiple operations.
|
|
131
|
+
- Parameters: `sessionId`, `viewport`, `shouldRunInHeadless`
|
|
132
|
+
- Sessions auto-expire after 3 minutes of inactivity
|
|
133
|
+
|
|
134
|
+
#### `navigate-session`
|
|
135
|
+
Navigates to a URL in an existing session.
|
|
136
|
+
- Parameters: `sessionId`, `url`
|
|
137
|
+
|
|
138
|
+
#### `click-session` / `click-session-by-text`
|
|
139
|
+
Click elements within a session.
|
|
80
140
|
|
|
81
|
-
|
|
141
|
+
#### `type-session` / `type-session-by-label`
|
|
142
|
+
Type text within a session.
|
|
82
143
|
|
|
83
|
-
|
|
144
|
+
#### `scan-session`
|
|
145
|
+
Run accessibility scan on current page in session.
|
|
146
|
+
|
|
147
|
+
#### `analyze-session`
|
|
148
|
+
Analyze current page in session.
|
|
149
|
+
|
|
150
|
+
#### `close-session`
|
|
151
|
+
Close a browser session.
|
|
152
|
+
|
|
153
|
+
#### `list-sessions`
|
|
154
|
+
List all active browser sessions.
|
|
155
|
+
|
|
156
|
+
## Usage Examples
|
|
157
|
+
|
|
158
|
+
### Basic Accessibility Scan
|
|
159
|
+
```
|
|
160
|
+
Could you scan example.com for WCAG 2.1 AA compliance issues?
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Color Contrast Check
|
|
84
164
|
```
|
|
85
|
-
|
|
165
|
+
Please check example.com for color contrast accessibility issues (cat.color).
|
|
86
166
|
```
|
|
87
167
|
|
|
88
|
-
|
|
168
|
+
### Multi-step Workflow with Sessions
|
|
89
169
|
```
|
|
90
|
-
|
|
170
|
+
1. Create a session and navigate to example.com
|
|
171
|
+
2. Click the "Sign In" button
|
|
172
|
+
3. Type "user@example.com" into the email field
|
|
173
|
+
4. Run an accessibility scan on the login page
|
|
174
|
+
5. Close the session
|
|
91
175
|
```
|
|
92
176
|
|
|
177
|
+
### Page Analysis
|
|
178
|
+
```
|
|
179
|
+
Can you analyze example.com and tell me what interactive elements are available?
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Smart Element Interaction
|
|
183
|
+
```
|
|
184
|
+
Navigate to example.com and click the button that says "Get Started"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Note:** All tools automatically save annotated screenshots to your downloads folder, with accessibility violations highlighted in red and numbered badges.
|
|
188
|
+
|
|
93
189
|
## Development
|
|
94
190
|
|
|
95
191
|
Clone and set up the project:
|
|
@@ -12,136 +12,368 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.AccessibilityScanner = void 0;
|
|
15
16
|
exports.scanViolations = scanViolations;
|
|
16
17
|
const playwright_1 = require("playwright");
|
|
17
18
|
const playwright_2 = require("@axe-core/playwright");
|
|
18
19
|
const node_path_1 = __importDefault(require("node:path"));
|
|
19
20
|
const node_os_1 = __importDefault(require("node:os"));
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
class AccessibilityScanner {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.browser = null;
|
|
24
|
+
this.context = null;
|
|
25
|
+
this.page = null;
|
|
26
|
+
this.defaultViewport = { width: 1920, height: 1080 };
|
|
27
|
+
this.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
28
|
+
}
|
|
29
|
+
initialize() {
|
|
30
|
+
return __awaiter(this, arguments, void 0, function* (shouldRunInHeadless = true) {
|
|
31
|
+
this.browser = yield playwright_1.chromium.launch({
|
|
32
|
+
headless: shouldRunInHeadless,
|
|
33
|
+
args: [
|
|
34
|
+
"--disable-blink-features=AutomationControlled",
|
|
35
|
+
"--disable-dev-shm-usage",
|
|
36
|
+
"--no-sandbox",
|
|
37
|
+
"--disable-setuid-sandbox",
|
|
38
|
+
],
|
|
39
|
+
});
|
|
31
40
|
});
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
}
|
|
42
|
+
createContext() {
|
|
43
|
+
return __awaiter(this, arguments, void 0, function* (viewport = this.defaultViewport) {
|
|
44
|
+
if (!this.browser) {
|
|
45
|
+
throw new Error("Browser not initialized. Call initialize() first.");
|
|
46
|
+
}
|
|
47
|
+
this.context = yield this.browser.newContext({
|
|
48
|
+
viewport,
|
|
49
|
+
userAgent: this.userAgent,
|
|
50
|
+
});
|
|
35
51
|
});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
margin: 2px !important;
|
|
44
|
-
}
|
|
45
|
-
.violation-number {
|
|
46
|
-
position: absolute !important;
|
|
47
|
-
top: -12px !important;
|
|
48
|
-
left: -12px !important;
|
|
49
|
-
background: #FF4444;
|
|
50
|
-
color: white !important;
|
|
51
|
-
width: 25px;
|
|
52
|
-
height: 25px;
|
|
53
|
-
border-radius: 50%;
|
|
54
|
-
display: flex !important;
|
|
55
|
-
align-items: center;
|
|
56
|
-
justify-content: center;
|
|
57
|
-
font-weight: bold;
|
|
58
|
-
font-size: 14px;
|
|
59
|
-
z-index: 10000;
|
|
60
|
-
}
|
|
61
|
-
.a11y-violation-info {
|
|
62
|
-
position: absolute !important;
|
|
63
|
-
background: #333333 !important;
|
|
64
|
-
color: white !important;
|
|
65
|
-
padding: 12px !important;
|
|
66
|
-
border-radius: 4px !important;
|
|
67
|
-
font-size: 14px !important;
|
|
68
|
-
max-width: 300px !important;
|
|
69
|
-
z-index: 9999 !important;
|
|
70
|
-
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
|
71
|
-
}
|
|
72
|
-
`,
|
|
52
|
+
}
|
|
53
|
+
createPage() {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
if (!this.context) {
|
|
56
|
+
throw new Error("Context not created. Call createContext() first.");
|
|
57
|
+
}
|
|
58
|
+
this.page = yield this.context.newPage();
|
|
73
59
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
60
|
+
}
|
|
61
|
+
navigateToUrl(url) {
|
|
62
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
63
|
+
if (!this.page) {
|
|
64
|
+
throw new Error("Page not created. Call createPage() first.");
|
|
65
|
+
}
|
|
66
|
+
yield this.page.goto(url);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
clickElement(selector) {
|
|
70
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
71
|
+
if (!this.page) {
|
|
72
|
+
throw new Error("Page not created. Call createPage() first.");
|
|
73
|
+
}
|
|
74
|
+
yield this.page.click(selector);
|
|
75
|
+
yield this.page.waitForLoadState('load');
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
typeText(selector, text) {
|
|
79
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
if (!this.page) {
|
|
81
|
+
throw new Error("Page not created. Call createPage() first.");
|
|
82
|
+
}
|
|
83
|
+
yield this.page.fill(selector, text);
|
|
84
|
+
yield this.page.waitForLoadState('load');
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
getElementByText(text, elementType) {
|
|
88
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
89
|
+
if (!this.page) {
|
|
90
|
+
throw new Error("Page not created. Call createPage() first.");
|
|
91
|
+
}
|
|
92
|
+
const elements = yield this.page.locator(elementType || '*', { hasText: text }).all();
|
|
93
|
+
if (elements.length === 0) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (elements.length === 1) {
|
|
97
|
+
return yield elements[0].evaluate(el => {
|
|
98
|
+
if (el.id)
|
|
99
|
+
return `#${el.id}`;
|
|
100
|
+
if (el.className)
|
|
101
|
+
return `.${el.className.split(' ')[0]}`;
|
|
102
|
+
return el.tagName.toLowerCase();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
for (let i = 0; i < elements.length; i++) {
|
|
106
|
+
const element = elements[i];
|
|
107
|
+
const isExactMatch = yield element.evaluate((el, searchText) => {
|
|
108
|
+
var _a, _b;
|
|
109
|
+
return ((_a = el.textContent) === null || _a === void 0 ? void 0 : _a.trim()) === searchText ||
|
|
110
|
+
('innerText' in el && ((_b = el.innerText) === null || _b === void 0 ? void 0 : _b.trim()) === searchText) ||
|
|
111
|
+
el.value === searchText;
|
|
112
|
+
}, text);
|
|
113
|
+
if (isExactMatch) {
|
|
114
|
+
return yield element.evaluate(el => {
|
|
115
|
+
var _a;
|
|
116
|
+
if (el.id)
|
|
117
|
+
return `#${el.id}`;
|
|
118
|
+
if (el.className)
|
|
119
|
+
return `.${el.className.split(' ')[0]}`;
|
|
120
|
+
return `:nth-of-type(${Array.from(((_a = el.parentElement) === null || _a === void 0 ? void 0 : _a.children) || []).indexOf(el) + 1})`;
|
|
118
121
|
});
|
|
119
|
-
violationCounter++;
|
|
120
122
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
}
|
|
124
|
+
return yield elements[0].evaluate(el => {
|
|
125
|
+
if (el.id)
|
|
126
|
+
return `#${el.id}`;
|
|
127
|
+
if (el.className)
|
|
128
|
+
return `.${el.className.split(' ')[0]}`;
|
|
129
|
+
return el.tagName.toLowerCase();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
analyzePage() {
|
|
134
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
135
|
+
if (!this.page) {
|
|
136
|
+
throw new Error("Page not created. Call createPage() first.");
|
|
137
|
+
}
|
|
138
|
+
return yield this.page.evaluate(() => {
|
|
139
|
+
const getElementInfo = (el) => {
|
|
140
|
+
const text = (el.textContent || '').trim();
|
|
141
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
142
|
+
const title = el.getAttribute('title');
|
|
143
|
+
const value = el.value;
|
|
144
|
+
const placeholder = el.placeholder;
|
|
145
|
+
return text || ariaLabel || title || value || placeholder || 'No text';
|
|
146
|
+
};
|
|
147
|
+
const buttons = Array.from(document.querySelectorAll('button, [role="button"], input[type="button"], input[type="submit"]'))
|
|
148
|
+
.map(el => getElementInfo(el))
|
|
149
|
+
.filter(text => text !== 'No text');
|
|
150
|
+
const links = Array.from(document.querySelectorAll('a[href]'))
|
|
151
|
+
.map(el => getElementInfo(el))
|
|
152
|
+
.filter(text => text !== 'No text');
|
|
153
|
+
const inputs = Array.from(document.querySelectorAll('input:not([type="button"]):not([type="submit"]), textarea, select'))
|
|
154
|
+
.map(el => {
|
|
155
|
+
var _a, _b, _c;
|
|
156
|
+
const label = (_c = (_b = (_a = el.labels) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.textContent) === null || _c === void 0 ? void 0 : _c.trim();
|
|
157
|
+
const placeholder = el.placeholder;
|
|
158
|
+
const name = el.getAttribute('name');
|
|
159
|
+
const id = el.getAttribute('id');
|
|
160
|
+
return label || placeholder || name || id || 'Unnamed input';
|
|
161
|
+
});
|
|
162
|
+
return { buttons, links, inputs };
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
clickElementByText(text, elementType) {
|
|
167
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
168
|
+
if (!this.page) {
|
|
169
|
+
throw new Error("Page not created. Call createPage() first.");
|
|
170
|
+
}
|
|
171
|
+
const selector = yield this.getElementByText(text, elementType);
|
|
172
|
+
if (!selector) {
|
|
173
|
+
const analysis = yield this.analyzePage();
|
|
174
|
+
throw new Error(`Element with text "${text}" not found. Available elements:\nButtons: ${analysis.buttons.join(', ')}\nLinks: ${analysis.links.join(', ')}`);
|
|
175
|
+
}
|
|
176
|
+
yield this.page.click(selector);
|
|
177
|
+
yield this.page.waitForLoadState('load');
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
typeTextByLabel(labelText, text) {
|
|
181
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
182
|
+
if (!this.page) {
|
|
183
|
+
throw new Error("Page not created. Call createPage() first.");
|
|
184
|
+
}
|
|
185
|
+
const inputSelector = yield this.page.evaluate((label) => {
|
|
186
|
+
const labels = Array.from(document.querySelectorAll('label'));
|
|
187
|
+
const matchingLabel = labels.find(l => { var _a; return (_a = l.textContent) === null || _a === void 0 ? void 0 : _a.trim().includes(label); });
|
|
188
|
+
if (matchingLabel) {
|
|
189
|
+
const forAttr = matchingLabel.getAttribute('for');
|
|
190
|
+
if (forAttr)
|
|
191
|
+
return `#${forAttr}`;
|
|
192
|
+
const input = matchingLabel.querySelector('input, textarea, select');
|
|
193
|
+
if (input && input.id)
|
|
194
|
+
return `#${input.id}`;
|
|
195
|
+
if (input)
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const inputsWithPlaceholder = Array.from(document.querySelectorAll(`input[placeholder*="${label}"], textarea[placeholder*="${label}"]`));
|
|
199
|
+
if (inputsWithPlaceholder.length > 0) {
|
|
200
|
+
const input = inputsWithPlaceholder[0];
|
|
201
|
+
if (input.id)
|
|
202
|
+
return `#${input.id}`;
|
|
123
203
|
}
|
|
204
|
+
return null;
|
|
205
|
+
}, labelText);
|
|
206
|
+
if (!inputSelector) {
|
|
207
|
+
const analysis = yield this.analyzePage();
|
|
208
|
+
throw new Error(`Input field with label "${labelText}" not found. Available inputs: ${analysis.inputs.join(', ')}`);
|
|
124
209
|
}
|
|
125
|
-
|
|
210
|
+
yield this.page.fill(inputSelector, text);
|
|
211
|
+
yield this.page.waitForLoadState('load');
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
addViolationStyles() {
|
|
215
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
216
|
+
if (!this.page) {
|
|
217
|
+
throw new Error("Page not created.");
|
|
218
|
+
}
|
|
219
|
+
yield this.page.addStyleTag({
|
|
220
|
+
content: `
|
|
221
|
+
.a11y-violation {
|
|
222
|
+
position: relative !important;
|
|
223
|
+
outline: 4px solid #FF4444 !important;
|
|
224
|
+
margin: 2px !important;
|
|
225
|
+
}
|
|
226
|
+
.violation-number {
|
|
227
|
+
position: absolute !important;
|
|
228
|
+
top: -12px !important;
|
|
229
|
+
left: -12px !important;
|
|
230
|
+
background: #FF4444;
|
|
231
|
+
color: white !important;
|
|
232
|
+
width: 25px;
|
|
233
|
+
height: 25px;
|
|
234
|
+
border-radius: 50%;
|
|
235
|
+
display: flex !important;
|
|
236
|
+
align-items: center;
|
|
237
|
+
justify-content: center;
|
|
238
|
+
font-weight: bold;
|
|
239
|
+
font-size: 14px;
|
|
240
|
+
z-index: 10000;
|
|
241
|
+
}
|
|
242
|
+
.a11y-violation-info {
|
|
243
|
+
position: absolute !important;
|
|
244
|
+
background: #333333 !important;
|
|
245
|
+
color: white !important;
|
|
246
|
+
padding: 12px !important;
|
|
247
|
+
border-radius: 4px !important;
|
|
248
|
+
font-size: 14px !important;
|
|
249
|
+
max-width: 300px !important;
|
|
250
|
+
z-index: 9999 !important;
|
|
251
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
|
252
|
+
}
|
|
253
|
+
`,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
highlightViolations(violations) {
|
|
258
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
259
|
+
if (!this.page) {
|
|
260
|
+
throw new Error("Page not created.");
|
|
261
|
+
}
|
|
262
|
+
let violationCounter = 1;
|
|
263
|
+
for (const violation of violations) {
|
|
264
|
+
for (const node of violation.nodes) {
|
|
265
|
+
try {
|
|
266
|
+
const targetSelector = node.target[0];
|
|
267
|
+
const selector = Array.isArray(targetSelector)
|
|
268
|
+
? targetSelector.join(" ")
|
|
269
|
+
: targetSelector;
|
|
270
|
+
yield this.page.evaluate(({ selector, violationData, counter }) => {
|
|
271
|
+
const elements = document.querySelectorAll(selector);
|
|
272
|
+
elements.forEach((element) => {
|
|
273
|
+
const numberBadge = document.createElement("div");
|
|
274
|
+
numberBadge.className = "violation-number";
|
|
275
|
+
numberBadge.textContent = counter.toString();
|
|
276
|
+
element.classList.add("a11y-violation");
|
|
277
|
+
element.appendChild(numberBadge);
|
|
278
|
+
const listItem = document.createElement("div");
|
|
279
|
+
listItem.style.marginBottom = "15px";
|
|
280
|
+
listItem.innerHTML = `
|
|
281
|
+
<div style="color: #FF4444; font-weight: bold;">
|
|
282
|
+
Violation #${counter}: ${violationData.impact.toUpperCase()}
|
|
283
|
+
</div>
|
|
284
|
+
<div style="margin: 5px 0; font-size: 14px;">
|
|
285
|
+
${violationData.description}
|
|
286
|
+
</div>
|
|
287
|
+
`;
|
|
288
|
+
document.body.appendChild(listItem);
|
|
289
|
+
const rect = element.getBoundingClientRect();
|
|
290
|
+
listItem.style.left = `${rect.left + window.scrollX}px`;
|
|
291
|
+
listItem.style.top = `${rect.bottom + window.scrollY + 10}px`;
|
|
292
|
+
});
|
|
293
|
+
}, {
|
|
294
|
+
selector: selector,
|
|
295
|
+
violationData: {
|
|
296
|
+
impact: violation.impact,
|
|
297
|
+
description: violation.description,
|
|
298
|
+
},
|
|
299
|
+
counter: violationCounter,
|
|
300
|
+
});
|
|
301
|
+
violationCounter++;
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
console.log(`Failed to highlight element: ${error}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
generateReport(violations) {
|
|
311
|
+
var _a;
|
|
126
312
|
let reportCounter = 1;
|
|
127
313
|
const report = [];
|
|
128
|
-
for (const violation of
|
|
314
|
+
for (const violation of violations) {
|
|
129
315
|
for (const node of violation.nodes) {
|
|
130
316
|
report.push({
|
|
131
317
|
index: reportCounter++,
|
|
132
318
|
element: node.target[0],
|
|
133
319
|
impactLevel: violation.impact,
|
|
134
320
|
description: violation.description,
|
|
135
|
-
wcagCriteria: (_a = violation.tags) === null || _a === void 0 ? void 0 : _a.join(", "),
|
|
321
|
+
wcagCriteria: ((_a = violation.tags) === null || _a === void 0 ? void 0 : _a.join(", ")) || "",
|
|
136
322
|
});
|
|
137
323
|
}
|
|
138
324
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
325
|
+
return report;
|
|
326
|
+
}
|
|
327
|
+
takeScreenshot() {
|
|
328
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
329
|
+
if (!this.page) {
|
|
330
|
+
throw new Error("Page not created.");
|
|
331
|
+
}
|
|
332
|
+
const filePath = node_path_1.default.join(node_path_1.default.join(node_os_1.default.homedir(), "Downloads"), `a11y-report-${Date.now()}.png`);
|
|
333
|
+
const screenshot = yield this.page.screenshot({
|
|
334
|
+
path: filePath,
|
|
335
|
+
});
|
|
336
|
+
return screenshot.toString("base64");
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
scanViolations(violationsTags) {
|
|
340
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
341
|
+
if (!this.page) {
|
|
342
|
+
throw new Error("Page not created.");
|
|
343
|
+
}
|
|
344
|
+
yield this.addViolationStyles();
|
|
345
|
+
const axe = new playwright_2.AxeBuilder({ page: this.page }).withTags(violationsTags);
|
|
346
|
+
const results = yield axe.analyze();
|
|
347
|
+
yield this.highlightViolations(results.violations);
|
|
348
|
+
const report = this.generateReport(results.violations);
|
|
349
|
+
const base64Screenshot = yield this.takeScreenshot();
|
|
350
|
+
return { report, base64Screenshot };
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
cleanup() {
|
|
354
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
355
|
+
if (this.browser) {
|
|
356
|
+
yield this.browser.close();
|
|
357
|
+
this.browser = null;
|
|
358
|
+
this.context = null;
|
|
359
|
+
this.page = null;
|
|
360
|
+
}
|
|
142
361
|
});
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
exports.AccessibilityScanner = AccessibilityScanner;
|
|
365
|
+
function scanViolations(url_1, violationsTags_1) {
|
|
366
|
+
return __awaiter(this, arguments, void 0, function* (url, violationsTags, viewport = { width: 1920, height: 1080 }, shouldRunInHeadless = true) {
|
|
367
|
+
const scanner = new AccessibilityScanner();
|
|
368
|
+
try {
|
|
369
|
+
yield scanner.initialize(shouldRunInHeadless);
|
|
370
|
+
yield scanner.createContext(viewport);
|
|
371
|
+
yield scanner.createPage();
|
|
372
|
+
yield scanner.navigateToUrl(url);
|
|
373
|
+
return yield scanner.scanViolations(violationsTags);
|
|
374
|
+
}
|
|
375
|
+
finally {
|
|
376
|
+
yield scanner.cleanup();
|
|
377
|
+
}
|
|
146
378
|
});
|
|
147
379
|
}
|