ally-a11y 1.0.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/ACCESSIBILITY.md +205 -0
- package/LICENSE +21 -0
- package/README.md +940 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +528 -0
- package/dist/commands/audit-palette.d.ts +18 -0
- package/dist/commands/audit-palette.js +613 -0
- package/dist/commands/auto-pr.d.ts +19 -0
- package/dist/commands/auto-pr.js +434 -0
- package/dist/commands/badge.d.ts +11 -0
- package/dist/commands/badge.js +143 -0
- package/dist/commands/completion.d.ts +4 -0
- package/dist/commands/completion.js +185 -0
- package/dist/commands/crawl.d.ts +12 -0
- package/dist/commands/crawl.js +249 -0
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/doctor.js +233 -0
- package/dist/commands/explain.d.ts +12 -0
- package/dist/commands/explain.js +233 -0
- package/dist/commands/fix.d.ts +13 -0
- package/dist/commands/fix.js +668 -0
- package/dist/commands/health.d.ts +11 -0
- package/dist/commands/health.js +367 -0
- package/dist/commands/history.d.ts +10 -0
- package/dist/commands/history.js +191 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +164 -0
- package/dist/commands/learn.d.ts +8 -0
- package/dist/commands/learn.js +592 -0
- package/dist/commands/pr-check.d.ts +12 -0
- package/dist/commands/pr-check.js +270 -0
- package/dist/commands/report.d.ts +11 -0
- package/dist/commands/report.js +375 -0
- package/dist/commands/scan-storybook.d.ts +18 -0
- package/dist/commands/scan-storybook.js +402 -0
- package/dist/commands/scan.d.ts +25 -0
- package/dist/commands/scan.js +673 -0
- package/dist/commands/stats.d.ts +5 -0
- package/dist/commands/stats.js +137 -0
- package/dist/commands/tree.d.ts +12 -0
- package/dist/commands/tree.js +635 -0
- package/dist/commands/triage.d.ts +13 -0
- package/dist/commands/triage.js +327 -0
- package/dist/commands/watch.d.ts +17 -0
- package/dist/commands/watch.js +302 -0
- package/dist/types/index.d.ts +60 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/baseline.d.ts +62 -0
- package/dist/utils/baseline.js +169 -0
- package/dist/utils/browser.d.ts +78 -0
- package/dist/utils/browser.js +239 -0
- package/dist/utils/cache.d.ts +76 -0
- package/dist/utils/cache.js +178 -0
- package/dist/utils/config.d.ts +102 -0
- package/dist/utils/config.js +237 -0
- package/dist/utils/converters.d.ts +77 -0
- package/dist/utils/converters.js +200 -0
- package/dist/utils/copilot.d.ts +36 -0
- package/dist/utils/copilot.js +139 -0
- package/dist/utils/detect.d.ts +22 -0
- package/dist/utils/detect.js +197 -0
- package/dist/utils/enhanced-errors.d.ts +46 -0
- package/dist/utils/enhanced-errors.js +295 -0
- package/dist/utils/errors.d.ts +31 -0
- package/dist/utils/errors.js +149 -0
- package/dist/utils/fix-patterns.d.ts +56 -0
- package/dist/utils/fix-patterns.js +529 -0
- package/dist/utils/history-tracking.d.ts +94 -0
- package/dist/utils/history-tracking.js +230 -0
- package/dist/utils/history.d.ts +42 -0
- package/dist/utils/history.js +255 -0
- package/dist/utils/impact-scores.d.ts +44 -0
- package/dist/utils/impact-scores.js +257 -0
- package/dist/utils/retry.d.ts +24 -0
- package/dist/utils/retry.js +76 -0
- package/dist/utils/scanner.d.ts +74 -0
- package/dist/utils/scanner.js +606 -0
- package/dist/utils/scanner.test.d.ts +4 -0
- package/dist/utils/scanner.test.js +162 -0
- package/dist/utils/ui.d.ts +44 -0
- package/dist/utils/ui.js +276 -0
- package/mcp-server/dist/index.d.ts +8 -0
- package/mcp-server/dist/index.js +1923 -0
- package/package.json +88 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impact Scoring System
|
|
3
|
+
*
|
|
4
|
+
* Scores accessibility violations by real-world user impact and business cost.
|
|
5
|
+
* Research shows developers are overwhelmed by violations - impact scoring
|
|
6
|
+
* helps prioritize what actually matters.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Base impact scores for violation types (0-100)
|
|
10
|
+
* Based on real-world user impact data
|
|
11
|
+
*/
|
|
12
|
+
const BASE_IMPACT_SCORES = {
|
|
13
|
+
// CRITICAL - Blocks core functionality (95-100)
|
|
14
|
+
'button-name': 98, // Can't use buttons at all
|
|
15
|
+
'label': 97, // Can't complete forms
|
|
16
|
+
'link-name': 96, // Can't navigate
|
|
17
|
+
'aria-required-parent': 95, // Breaks component completely
|
|
18
|
+
'aria-required-children': 95,
|
|
19
|
+
'keyboard-trap': 100, // Completely stuck
|
|
20
|
+
// HIGH - Severely impacts usability (75-94)
|
|
21
|
+
'html-has-lang': 89, // Screen reader uses wrong language
|
|
22
|
+
'frame-title': 88, // Can't understand iframe content
|
|
23
|
+
'form-field-multiple-labels': 87,
|
|
24
|
+
'duplicate-id-active': 86, // Focus management broken
|
|
25
|
+
'aria-hidden-focus': 85, // Can focus hidden elements
|
|
26
|
+
'select-name': 84,
|
|
27
|
+
'bypass': 83, // Must tab through entire nav
|
|
28
|
+
// MEDIUM - Reduces accessibility (40-74)
|
|
29
|
+
'image-alt': 72, // Can't understand images
|
|
30
|
+
'color-contrast': 70, // Hard to read text
|
|
31
|
+
'heading-order': 65, // Confusing structure
|
|
32
|
+
'landmark-one-main': 63, // Can't find main content
|
|
33
|
+
'meta-viewport': 62, // Can't zoom
|
|
34
|
+
'document-title': 60, // Don't know what page
|
|
35
|
+
'list': 58,
|
|
36
|
+
'listitem': 58,
|
|
37
|
+
'definition-list': 55,
|
|
38
|
+
'dlitem': 55,
|
|
39
|
+
// LOW - Best practice violations (0-39)
|
|
40
|
+
'region': 35, // Missing landmarks
|
|
41
|
+
'aria-allowed-attr': 32, // Invalid ARIA (still works)
|
|
42
|
+
'aria-valid-attr-value': 30,
|
|
43
|
+
'meta-refresh': 28, // Annoying but not blocking
|
|
44
|
+
'tabindex': 25, // Non-standard tab order
|
|
45
|
+
'accesskeys': 20, // Rarely used feature
|
|
46
|
+
'empty-heading': 18, // Confusing but not blocking
|
|
47
|
+
'p-as-heading': 15, // Semantic issue
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Context multipliers adjust score based on page type
|
|
51
|
+
*/
|
|
52
|
+
const CONTEXT_MULTIPLIERS = {
|
|
53
|
+
'button-name': {
|
|
54
|
+
checkout: 1.5, // Critical in checkout flow
|
|
55
|
+
form: 1.3,
|
|
56
|
+
navigation: 1.2,
|
|
57
|
+
default: 1.0,
|
|
58
|
+
},
|
|
59
|
+
'label': {
|
|
60
|
+
checkout: 1.5, // Can't complete purchase
|
|
61
|
+
form: 1.4,
|
|
62
|
+
default: 1.0,
|
|
63
|
+
},
|
|
64
|
+
'link-name': {
|
|
65
|
+
navigation: 1.3, // Critical for navigation
|
|
66
|
+
landing: 1.2,
|
|
67
|
+
default: 1.0,
|
|
68
|
+
},
|
|
69
|
+
'color-contrast': {
|
|
70
|
+
checkout: 1.3, // Must read CTA buttons
|
|
71
|
+
form: 1.2,
|
|
72
|
+
default: 1.0,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* User impact data - who is affected by each violation
|
|
77
|
+
*/
|
|
78
|
+
const AFFECTED_USERS = {
|
|
79
|
+
'button-name': ['Screen reader users', 'Voice control users'],
|
|
80
|
+
'label': ['Screen reader users', 'Voice control users', 'Cognitive disabilities'],
|
|
81
|
+
'link-name': ['Screen reader users', 'Voice control users'],
|
|
82
|
+
'image-alt': ['Screen reader users', 'Users on slow connections'],
|
|
83
|
+
'color-contrast': ['Low vision users', 'Color blind users', 'Users in bright sunlight'],
|
|
84
|
+
'html-has-lang': ['Screen reader users', 'Translation tool users'],
|
|
85
|
+
'keyboard-trap': ['Keyboard-only users', 'Motor disabilities', 'Power users'],
|
|
86
|
+
'bypass': ['Keyboard-only users', 'Screen reader users'],
|
|
87
|
+
'heading-order': ['Screen reader users', 'Cognitive disabilities'],
|
|
88
|
+
'frame-title': ['Screen reader users'],
|
|
89
|
+
'meta-viewport': ['Mobile users', 'Low vision users'],
|
|
90
|
+
'document-title': ['Screen reader users', 'Multiple tab users'],
|
|
91
|
+
'region': ['Screen reader users'],
|
|
92
|
+
'landmark-one-main': ['Screen reader users'],
|
|
93
|
+
'aria-hidden-focus': ['Keyboard-only users', 'Screen reader users'],
|
|
94
|
+
'select-name': ['Screen reader users', 'Voice control users'],
|
|
95
|
+
'aria-required-parent': ['Screen reader users'],
|
|
96
|
+
'aria-required-children': ['Screen reader users'],
|
|
97
|
+
'duplicate-id-active': ['Screen reader users', 'Keyboard-only users'],
|
|
98
|
+
'form-field-multiple-labels': ['Screen reader users', 'Voice control users'],
|
|
99
|
+
'list': ['Screen reader users'],
|
|
100
|
+
'listitem': ['Screen reader users'],
|
|
101
|
+
'definition-list': ['Screen reader users'],
|
|
102
|
+
'dlitem': ['Screen reader users'],
|
|
103
|
+
'tabindex': ['Keyboard-only users'],
|
|
104
|
+
'meta-refresh': ['Screen reader users', 'Cognitive disabilities'],
|
|
105
|
+
'p-as-heading': ['Screen reader users'],
|
|
106
|
+
'empty-heading': ['Screen reader users'],
|
|
107
|
+
'accesskeys': ['Keyboard-only users'],
|
|
108
|
+
'aria-allowed-attr': ['Screen reader users'],
|
|
109
|
+
'aria-valid-attr-value': ['Screen reader users'],
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Business impact reasoning templates
|
|
113
|
+
*/
|
|
114
|
+
const BUSINESS_REASONING = {
|
|
115
|
+
'button-name': 'Users cannot activate buttons, blocking core actions like "Submit", "Buy Now", or "Sign Up"',
|
|
116
|
+
'label': 'Users cannot complete forms, preventing account creation, checkout, or data submission',
|
|
117
|
+
'link-name': 'Users cannot navigate your site, unable to determine link destinations or purposes',
|
|
118
|
+
'image-alt': 'Users miss critical visual information, including product images, diagrams, or icons',
|
|
119
|
+
'color-contrast': 'Text is illegible for users with low vision or in bright environments',
|
|
120
|
+
'html-has-lang': 'Screen readers pronounce content incorrectly, making text unintelligible',
|
|
121
|
+
'keyboard-trap': 'Users become stuck in UI elements and cannot escape to continue browsing',
|
|
122
|
+
'bypass': 'Users must tab through dozens of navigation links before reaching main content',
|
|
123
|
+
'heading-order': 'Page structure is confusing, making content difficult to navigate and understand',
|
|
124
|
+
'frame-title': 'Users cannot understand iframe content purpose without entering the iframe',
|
|
125
|
+
'meta-viewport': 'Users cannot zoom text to readable size on mobile devices',
|
|
126
|
+
'document-title': 'Users cannot identify page in browser tabs or history',
|
|
127
|
+
'region': 'Users cannot quickly navigate to different page sections',
|
|
128
|
+
'landmark-one-main': 'Users cannot quickly jump to main content area',
|
|
129
|
+
'aria-hidden-focus': 'Hidden elements receive focus, confusing keyboard navigation',
|
|
130
|
+
'select-name': 'Users cannot identify dropdown purpose or make selections confidently',
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Estimated percentage of users affected
|
|
134
|
+
*/
|
|
135
|
+
const ESTIMATED_USER_IMPACT = {
|
|
136
|
+
'button-name': '15-20% of users',
|
|
137
|
+
'label': '15-20% of users',
|
|
138
|
+
'link-name': '15-20% of users',
|
|
139
|
+
'image-alt': '10-15% of users',
|
|
140
|
+
'color-contrast': '20-30% of users',
|
|
141
|
+
'html-has-lang': '10-15% of users',
|
|
142
|
+
'keyboard-trap': '8-12% of users',
|
|
143
|
+
'bypass': '8-12% of users',
|
|
144
|
+
'heading-order': '10-15% of users',
|
|
145
|
+
'frame-title': '5-10% of users',
|
|
146
|
+
'meta-viewport': '15-25% of users',
|
|
147
|
+
'document-title': '10-15% of users',
|
|
148
|
+
'default': '5-15% of users',
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Calculate WCAG level for violation
|
|
152
|
+
*/
|
|
153
|
+
function getWCAGLevel(violation) {
|
|
154
|
+
const tags = violation.tags || [];
|
|
155
|
+
if (tags.includes('wcag2aaa') || tags.includes('wcag21aaa') || tags.includes('wcag22aaa')) {
|
|
156
|
+
return 'AAA';
|
|
157
|
+
}
|
|
158
|
+
if (tags.includes('wcag2aa') || tags.includes('wcag21aa') || tags.includes('wcag22aa')) {
|
|
159
|
+
return 'AA';
|
|
160
|
+
}
|
|
161
|
+
return 'A';
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Detect page context from HTML content
|
|
165
|
+
*/
|
|
166
|
+
export function detectPageContext(html) {
|
|
167
|
+
const hasCheckoutKeywords = /checkout|cart|payment|order|purchase/i.test(html);
|
|
168
|
+
const hasFormKeywords = /<form|<input|<select|<textarea/i.test(html);
|
|
169
|
+
const hasNavKeywords = /<nav|navigation|menu/i.test(html);
|
|
170
|
+
let pageType = 'content';
|
|
171
|
+
if (hasCheckoutKeywords) {
|
|
172
|
+
pageType = 'checkout';
|
|
173
|
+
}
|
|
174
|
+
else if (hasFormKeywords) {
|
|
175
|
+
pageType = 'form';
|
|
176
|
+
}
|
|
177
|
+
else if (hasNavKeywords) {
|
|
178
|
+
pageType = 'navigation';
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
pageType,
|
|
182
|
+
hasForm: hasFormKeywords,
|
|
183
|
+
hasCheckout: hasCheckoutKeywords,
|
|
184
|
+
isNavigation: hasNavKeywords,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Calculate impact score for a violation
|
|
189
|
+
*/
|
|
190
|
+
export function calculateImpactScore(violation, context = {}) {
|
|
191
|
+
const violationId = violation.id;
|
|
192
|
+
// Get base score (default to 50 for unknown violations)
|
|
193
|
+
const baseScore = BASE_IMPACT_SCORES[violationId] || 50;
|
|
194
|
+
// Get context multiplier
|
|
195
|
+
const pageType = context.pageType || 'default';
|
|
196
|
+
const multipliers = CONTEXT_MULTIPLIERS[violationId] || {};
|
|
197
|
+
const multiplier = multipliers[pageType] || multipliers.default || 1.0;
|
|
198
|
+
// Calculate final score (capped at 100)
|
|
199
|
+
const score = Math.min(100, Math.round(baseScore * multiplier));
|
|
200
|
+
// Determine business impact category
|
|
201
|
+
let businessImpact;
|
|
202
|
+
if (score >= 95) {
|
|
203
|
+
businessImpact = 'critical';
|
|
204
|
+
}
|
|
205
|
+
else if (score >= 75) {
|
|
206
|
+
businessImpact = 'high';
|
|
207
|
+
}
|
|
208
|
+
else if (score >= 40) {
|
|
209
|
+
businessImpact = 'medium';
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
businessImpact = 'low';
|
|
213
|
+
}
|
|
214
|
+
// Get affected user groups
|
|
215
|
+
const affectedUsers = AFFECTED_USERS[violationId] || ['Assistive technology users'];
|
|
216
|
+
// Get or generate reasoning
|
|
217
|
+
const reasoning = BUSINESS_REASONING[violationId] ||
|
|
218
|
+
`${violation.help}. This affects user experience for people using assistive technologies.`;
|
|
219
|
+
// Get estimated user impact
|
|
220
|
+
const estimatedUsers = ESTIMATED_USER_IMPACT[violationId] || ESTIMATED_USER_IMPACT.default;
|
|
221
|
+
// Get WCAG level
|
|
222
|
+
const wcagLevel = getWCAGLevel(violation);
|
|
223
|
+
return {
|
|
224
|
+
score,
|
|
225
|
+
reasoning,
|
|
226
|
+
affectedUsers,
|
|
227
|
+
businessImpact,
|
|
228
|
+
estimatedUsers,
|
|
229
|
+
wcagLevel,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Sort violations by impact score (highest first)
|
|
234
|
+
*/
|
|
235
|
+
export function sortByImpact(violations, context = {}) {
|
|
236
|
+
const scored = violations.map((violation) => ({
|
|
237
|
+
violation,
|
|
238
|
+
impact: calculateImpactScore(violation, context),
|
|
239
|
+
}));
|
|
240
|
+
return scored.sort((a, b) => b.impact.score - a.impact.score);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Group violations by impact category
|
|
244
|
+
*/
|
|
245
|
+
export function groupByImpact(violations, context = {}) {
|
|
246
|
+
const sorted = sortByImpact(violations, context);
|
|
247
|
+
const groups = {
|
|
248
|
+
critical: [],
|
|
249
|
+
high: [],
|
|
250
|
+
medium: [],
|
|
251
|
+
low: [],
|
|
252
|
+
};
|
|
253
|
+
for (const item of sorted) {
|
|
254
|
+
groups[item.impact.businessImpact].push(item);
|
|
255
|
+
}
|
|
256
|
+
return groups;
|
|
257
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry utility with exponential backoff for transient errors
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Check if an error is transient (network/timeout related)
|
|
6
|
+
*/
|
|
7
|
+
export declare function isTransientError(error: unknown): boolean;
|
|
8
|
+
export interface RetryOptions {
|
|
9
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
10
|
+
maxRetries?: number;
|
|
11
|
+
/** Base delay in ms for exponential backoff (default: 1000) */
|
|
12
|
+
baseDelayMs?: number;
|
|
13
|
+
/** Callback for logging retry attempts */
|
|
14
|
+
onRetry?: (attempt: number, error: Error, delayMs: number) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Execute a function with exponential backoff retry for transient errors
|
|
18
|
+
*
|
|
19
|
+
* @param fn The async function to execute
|
|
20
|
+
* @param options Retry configuration
|
|
21
|
+
* @returns The result of the function
|
|
22
|
+
* @throws The last error if all retries are exhausted, or immediately for non-transient errors
|
|
23
|
+
*/
|
|
24
|
+
export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry utility with exponential backoff for transient errors
|
|
3
|
+
*/
|
|
4
|
+
/** Errors that are considered transient and should be retried */
|
|
5
|
+
const TRANSIENT_ERROR_PATTERNS = [
|
|
6
|
+
'ECONNRESET',
|
|
7
|
+
'ECONNREFUSED',
|
|
8
|
+
'ETIMEDOUT',
|
|
9
|
+
'ESOCKETTIMEDOUT',
|
|
10
|
+
'ENOTFOUND',
|
|
11
|
+
'EAI_AGAIN',
|
|
12
|
+
'net::ERR_',
|
|
13
|
+
'Navigation timeout',
|
|
14
|
+
'Timeout exceeded',
|
|
15
|
+
'TimeoutError',
|
|
16
|
+
'socket hang up',
|
|
17
|
+
'read ECONNRESET',
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Check if an error is transient (network/timeout related)
|
|
21
|
+
*/
|
|
22
|
+
export function isTransientError(error) {
|
|
23
|
+
if (!(error instanceof Error))
|
|
24
|
+
return false;
|
|
25
|
+
const message = error.message.toLowerCase();
|
|
26
|
+
const name = error.name.toLowerCase();
|
|
27
|
+
for (const pattern of TRANSIENT_ERROR_PATTERNS) {
|
|
28
|
+
if (message.includes(pattern.toLowerCase()) || name.includes(pattern.toLowerCase())) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Execute a function with exponential backoff retry for transient errors
|
|
36
|
+
*
|
|
37
|
+
* @param fn The async function to execute
|
|
38
|
+
* @param options Retry configuration
|
|
39
|
+
* @returns The result of the function
|
|
40
|
+
* @throws The last error if all retries are exhausted, or immediately for non-transient errors
|
|
41
|
+
*/
|
|
42
|
+
export async function withRetry(fn, options = {}) {
|
|
43
|
+
const { maxRetries = 3, baseDelayMs = 1000, onRetry, } = options;
|
|
44
|
+
let lastError;
|
|
45
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
46
|
+
try {
|
|
47
|
+
return await fn();
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
51
|
+
lastError = err;
|
|
52
|
+
// Don't retry non-transient errors
|
|
53
|
+
if (!isTransientError(err)) {
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
// Don't retry if we've exhausted attempts
|
|
57
|
+
if (attempt >= maxRetries) {
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
// Calculate delay with exponential backoff: 1s, 2s, 4s
|
|
61
|
+
const delayMs = baseDelayMs * Math.pow(2, attempt);
|
|
62
|
+
if (onRetry) {
|
|
63
|
+
onRetry(attempt + 1, err, delayMs);
|
|
64
|
+
}
|
|
65
|
+
await sleep(delayMs);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// This should never be reached, but TypeScript needs it
|
|
69
|
+
throw lastError ?? new Error('Retry failed');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Sleep for the specified duration
|
|
73
|
+
*/
|
|
74
|
+
function sleep(ms) {
|
|
75
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
76
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility scanner using axe-core with Puppeteer or Playwright
|
|
3
|
+
*/
|
|
4
|
+
import type { ScanResult, AllyReport, ReportSummary } from '../types/index.js';
|
|
5
|
+
import { type BrowserType } from './browser.js';
|
|
6
|
+
export type { BrowserType } from './browser.js';
|
|
7
|
+
export type WcagStandard = 'wcag2a' | 'wcag2aa' | 'wcag2aaa' | 'wcag21a' | 'wcag21aa' | 'wcag21aaa' | 'wcag22aa' | 'section508' | 'best-practice';
|
|
8
|
+
export declare const standardToTags: Record<WcagStandard, string[]>;
|
|
9
|
+
export declare const DEFAULT_STANDARD: WcagStandard;
|
|
10
|
+
export type ColorBlindnessType = 'protanopia' | 'deuteranopia' | 'tritanopia';
|
|
11
|
+
/** Default page load timeout in milliseconds */
|
|
12
|
+
export declare const DEFAULT_TIMEOUT = 30000;
|
|
13
|
+
/** Default batch size for parallel scanning */
|
|
14
|
+
export declare const DEFAULT_BATCH_SIZE = 4;
|
|
15
|
+
/**
|
|
16
|
+
* Result from parallel scanning - includes file path for error tracking
|
|
17
|
+
*/
|
|
18
|
+
export interface ParallelScanResult {
|
|
19
|
+
result?: ScanResult;
|
|
20
|
+
error?: {
|
|
21
|
+
path: string;
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Callback for progress updates during parallel scanning
|
|
27
|
+
*/
|
|
28
|
+
export type ScanProgressCallback = (completed: number, total: number, currentFile?: string) => void;
|
|
29
|
+
export declare class AccessibilityScanner {
|
|
30
|
+
private browser;
|
|
31
|
+
private browserAdapter;
|
|
32
|
+
private timeout;
|
|
33
|
+
private browserType;
|
|
34
|
+
private usePlaywright;
|
|
35
|
+
constructor(timeout?: number, browserType?: BrowserType);
|
|
36
|
+
init(): Promise<void>;
|
|
37
|
+
close(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Get the browser type being used
|
|
40
|
+
*/
|
|
41
|
+
getBrowserType(): BrowserType;
|
|
42
|
+
scanHtmlFile(filePath: string, standard?: WcagStandard): Promise<ScanResult>;
|
|
43
|
+
scanUrl(url: string, standard?: WcagStandard): Promise<ScanResult>;
|
|
44
|
+
scanHtmlString(html: string, identifier?: string, standard?: WcagStandard): Promise<ScanResult>;
|
|
45
|
+
/**
|
|
46
|
+
* Scan multiple HTML files in parallel batches for improved performance
|
|
47
|
+
* @param files Array of file paths to scan
|
|
48
|
+
* @param standard WCAG standard to use for scanning
|
|
49
|
+
* @param batchSize Number of files to scan concurrently (default: 4)
|
|
50
|
+
* @param onProgress Optional callback for progress updates
|
|
51
|
+
* @returns Array of scan results (successful) and errors (failed)
|
|
52
|
+
*/
|
|
53
|
+
scanHtmlFilesParallel(files: string[], standard?: WcagStandard, batchSize?: number, onProgress?: ScanProgressCallback): Promise<{
|
|
54
|
+
results: ScanResult[];
|
|
55
|
+
errors: Array<{
|
|
56
|
+
path: string;
|
|
57
|
+
error: string;
|
|
58
|
+
}>;
|
|
59
|
+
}>;
|
|
60
|
+
/**
|
|
61
|
+
* Extract all anchor links from a URL
|
|
62
|
+
* This method provides type-safe access to browser pages for link extraction
|
|
63
|
+
*/
|
|
64
|
+
extractLinks(url: string): Promise<string[]>;
|
|
65
|
+
/**
|
|
66
|
+
* Take a screenshot of a URL with color blindness simulation applied
|
|
67
|
+
*/
|
|
68
|
+
simulateColorBlindness(url: string, type: ColorBlindnessType, outputPath: string): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
export declare function findHtmlFiles(targetPath: string, extraIgnore?: string[]): Promise<string[]>;
|
|
71
|
+
export declare function findComponentFiles(targetPath: string, extraIgnore?: string[]): Promise<string[]>;
|
|
72
|
+
export declare function calculateScore(results: ScanResult[]): number;
|
|
73
|
+
export declare function generateSummary(results: ScanResult[]): ReportSummary;
|
|
74
|
+
export declare function createReport(results: ScanResult[]): AllyReport;
|