wcag-scanner 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.
@@ -0,0 +1,390 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Check image accessibility (alt text, etc.)
5
+ */
6
+ exports.default = {
7
+ /**
8
+ * Run image accessibility checks
9
+ * @param document DOM document
10
+ * @param window Browser window
11
+ * @param options Scanner options
12
+ * @returns Promise<ScanResults> Results from image checks
13
+ */
14
+ async check(document, window, options) {
15
+ const results = {
16
+ passes: [],
17
+ violations: [],
18
+ warnings: []
19
+ };
20
+ // Check <img> elements
21
+ checkImageElements(document, results);
22
+ // Check <svg> elements
23
+ checkSvgElements(document, results);
24
+ // Check background images
25
+ checkBackgroundImages(document, window, results);
26
+ // Check image maps
27
+ checkImageMaps(document, results);
28
+ return results;
29
+ }
30
+ };
31
+ /**
32
+ * Check all <img> elements for accessibility issues
33
+ * @param document DOM document
34
+ * @param results DOM results
35
+ */
36
+ function checkImageElements(document, results) {
37
+ const images = document.querySelectorAll('img');
38
+ images.forEach(img => {
39
+ const info = {
40
+ tagName: 'img',
41
+ id: img.id || null,
42
+ className: img.className || null,
43
+ src: img.getAttribute('src') || null
44
+ };
45
+ // Check for alt attribute
46
+ if (!img.hasAttribute('alt')) {
47
+ results.violations.push({
48
+ rule: 'img-alt',
49
+ element: info,
50
+ impact: 'critical',
51
+ description: 'Image is missing alt text',
52
+ snippet: img.outerHTML,
53
+ wcag: ['1.1.1'],
54
+ help: 'Images must have alternative text',
55
+ helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html'
56
+ });
57
+ }
58
+ else {
59
+ const altText = img.getAttribute('alt') || '';
60
+ if (altText === '') {
61
+ // Empty alt is valid for decorative images
62
+ if (!isLikelyDecorativeImage(img)) {
63
+ results.warnings.push({
64
+ rule: 'img-alt-decorative',
65
+ element: info,
66
+ impact: 'moderate',
67
+ description: 'Image has empty alt text but may not be decorative',
68
+ snippet: img.outerHTML,
69
+ wcag: ['1.1.1'],
70
+ help: 'Verify this image is decorative; if not, add descriptive alt text'
71
+ });
72
+ }
73
+ else {
74
+ results.passes.push({
75
+ rule: 'img-alt-decorative',
76
+ element: info,
77
+ description: 'Decorative image has appropriate empty alt text',
78
+ snippet: img.outerHTML
79
+ });
80
+ }
81
+ }
82
+ else if (hasGenericAltText(altText)) {
83
+ // Check for generic/placeholder alt text
84
+ results.warnings.push({
85
+ rule: 'img-alt-generic',
86
+ element: info,
87
+ impact: 'moderate',
88
+ description: 'Image may have generic/placeholder alt text',
89
+ snippet: img.outerHTML,
90
+ wcag: ['1.1.1'],
91
+ help: 'Replace generic alt text with specific description'
92
+ });
93
+ }
94
+ else if (altText.length > 125) {
95
+ // Long alt text warning
96
+ results.warnings.push({
97
+ rule: 'img-alt-long',
98
+ element: info,
99
+ impact: 'minor',
100
+ description: 'Alt text is unusually long (over 125 characters)',
101
+ snippet: img.outerHTML,
102
+ wcag: ['1.1.1'],
103
+ help: 'Consider using a more concise alt text or using a longdesc attribute'
104
+ });
105
+ }
106
+ else {
107
+ results.passes.push({
108
+ rule: 'img-alt',
109
+ element: info,
110
+ description: 'Image has appropriate alt text',
111
+ snippet: img.outerHTML
112
+ });
113
+ }
114
+ }
115
+ // Check for responsive images
116
+ if (!img.hasAttribute('width') || !img.hasAttribute('height')) {
117
+ results.warnings.push({
118
+ rule: 'img-dimensions',
119
+ element: info,
120
+ impact: 'minor',
121
+ description: 'Image is missing width and/or height attributes',
122
+ snippet: img.outerHTML,
123
+ help: 'Set explicit width and height to prevent layout shifts'
124
+ });
125
+ }
126
+ });
127
+ }
128
+ /**
129
+ * Check SVG elements for accessibility
130
+ * @param document DOM document
131
+ * @param results Scan results
132
+ */
133
+ function checkSvgElements(document, results) {
134
+ const svgs = document.querySelectorAll('svg');
135
+ svgs.forEach(svg => {
136
+ var _a, _b;
137
+ const info = {
138
+ tagName: 'svg',
139
+ id: svg.id || null,
140
+ className: ((_a = svg.classList) === null || _a === void 0 ? void 0 : _a.toString()) || null
141
+ };
142
+ // Check for role="img"
143
+ if (!svg.hasAttribute('role') || svg.getAttribute('role') !== 'img') {
144
+ results.warnings.push({
145
+ rule: 'svg-role',
146
+ element: info,
147
+ impact: 'moderate',
148
+ description: 'SVG element should have role="img"',
149
+ snippet: svg.outerHTML.slice(0, 150) + (svg.outerHTML.length > 150 ? '...' : ''),
150
+ wcag: ['1.1.1'],
151
+ help: 'Add role="img" to SVG elements'
152
+ });
153
+ }
154
+ // Check for accessible name via title or aria-label
155
+ const title = svg.querySelector('title');
156
+ const ariaLabel = svg.getAttribute('aria-label');
157
+ const ariaLabelledby = svg.getAttribute('aria-labelledby');
158
+ if (!title && !ariaLabel && !ariaLabelledby) {
159
+ results.violations.push({
160
+ rule: 'svg-accessible-name',
161
+ element: info,
162
+ impact: 'serious',
163
+ description: 'SVG lacks accessible name (title, aria-label, or aria-labelledby)',
164
+ snippet: svg.outerHTML.slice(0, 150) + (svg.outerHTML.length > 150 ? '...' : ''),
165
+ wcag: ['1.1.1'],
166
+ help: 'Add a <title> element or aria-label attribute to SVG'
167
+ });
168
+ }
169
+ else if (title && !((_b = title.textContent) === null || _b === void 0 ? void 0 : _b.trim())) {
170
+ results.violations.push({
171
+ rule: 'svg-title-empty',
172
+ element: info,
173
+ impact: 'serious',
174
+ description: 'SVG title element is empty',
175
+ snippet: svg.outerHTML.slice(0, 150) + (svg.outerHTML.length > 150 ? '...' : ''),
176
+ wcag: ['1.1.1'],
177
+ help: 'Add content to the SVG title element'
178
+ });
179
+ }
180
+ else {
181
+ results.passes.push({
182
+ rule: 'svg-accessible-name',
183
+ element: info,
184
+ description: 'SVG has an accessible name',
185
+ snippet: svg.outerHTML.slice(0, 150) + (svg.outerHTML.length > 150 ? '...' : '')
186
+ });
187
+ }
188
+ });
189
+ }
190
+ /**
191
+ * Check background images for accessibility issues
192
+ * @param document DOM document
193
+ * @param window Window object
194
+ * @param results Scan results
195
+ */
196
+ function checkBackgroundImages(document, window, results) {
197
+ // Find elements with background image
198
+ const allElements = document.querySelectorAll('*');
199
+ allElements.forEach(element => {
200
+ var _a, _b, _c;
201
+ const style = window.getComputedStyle(element);
202
+ const backgroundImage = style.backgroundImage;
203
+ // Skip if no background image or if it's "none"
204
+ if (!backgroundImage || backgroundImage === 'none') {
205
+ return;
206
+ }
207
+ // Skip if the element is hidden/decorative
208
+ if (isElementHidden(element) || element.getAttribute('aria-hidden') === 'true') {
209
+ return;
210
+ }
211
+ const info = {
212
+ tagName: element.tagName.toLowerCase(),
213
+ id: element.id || null,
214
+ className: ((_a = element.className) === null || _a === void 0 ? void 0 : _a.toString()) || null,
215
+ textContent: ((_b = element.textContent) === null || _b === void 0 ? void 0 : _b.substring(0, 50)) || null
216
+ };
217
+ // Check if meaningful background image has text alternative
218
+ const hasTextContent = ((_c = element.textContent) === null || _c === void 0 ? void 0 : _c.trim()) !== '';
219
+ const hasAriaLabel = element.hasAttribute('aria-label');
220
+ const hasAriaLabelledby = element.hasAttribute('aria-labelledby');
221
+ const hasTitle = element.hasAttribute('title');
222
+ if (!hasTextContent && !hasAriaLabel && !hasAriaLabelledby && !hasTitle) {
223
+ // Only warn if the background image looks like content (not decoration)
224
+ if (backgroundImage.includes('url(') && !isBackgroundLikelyDecorative(element)) {
225
+ results.warnings.push({
226
+ rule: 'background-image',
227
+ element: info,
228
+ impact: 'moderate',
229
+ description: 'Element with background image may need text alternative',
230
+ snippet: element.outerHTML.slice(0, 150) + (element.outerHTML.length > 150 ? '...' : ''),
231
+ wcag: ['1.1.1'],
232
+ help: 'If the background image conveys meaning, add text alternative via aria-label or text content'
233
+ });
234
+ }
235
+ }
236
+ });
237
+ }
238
+ /**
239
+ * Check image maps for accessibility
240
+ * @param document DOM document
241
+ * @param results Scan results
242
+ */
243
+ function checkImageMaps(document, results) {
244
+ const maps = document.querySelectorAll('map');
245
+ maps.forEach(map => {
246
+ const info = {
247
+ tagName: 'map',
248
+ id: map.id || null,
249
+ name: map.getAttribute('name') || null
250
+ };
251
+ // Check if the map is used
252
+ const usedByImages = document.querySelectorAll(`img[usemap="#${map.name}"]`);
253
+ if (usedByImages.length === 0) {
254
+ results.warnings.push({
255
+ rule: 'map-unused',
256
+ element: info,
257
+ impact: 'minor',
258
+ description: 'Image map appears to be unused',
259
+ snippet: map.outerHTML,
260
+ help: 'Remove unused image maps'
261
+ });
262
+ return;
263
+ }
264
+ // Check area elements
265
+ const areas = map.querySelectorAll('area');
266
+ areas.forEach(area => {
267
+ const areaInfo = {
268
+ tagName: 'area',
269
+ shape: area.getAttribute('shape') || null,
270
+ coords: area.getAttribute('coords') || null,
271
+ href: area.getAttribute('href') || null
272
+ };
273
+ // Check for alt text
274
+ if (!area.hasAttribute('alt')) {
275
+ results.violations.push({
276
+ rule: 'area-alt',
277
+ element: areaInfo,
278
+ impact: 'critical',
279
+ description: 'Area element in image map is missing alt text',
280
+ snippet: area.outerHTML,
281
+ wcag: ['1.1.1', '2.4.4'],
282
+ help: 'Add alt text to all area elements'
283
+ });
284
+ }
285
+ else {
286
+ results.passes.push({
287
+ rule: 'area-alt',
288
+ element: areaInfo,
289
+ description: 'Area element has alt text',
290
+ snippet: area.outerHTML
291
+ });
292
+ }
293
+ });
294
+ });
295
+ }
296
+ /**
297
+ * Check if an element is hidden
298
+ * @param element Element to check
299
+ */
300
+ function isElementHidden(element) {
301
+ // This is a simplified check - a real implementation would be more comprehensive
302
+ if (element.hasAttribute('hidden') || element.getAttribute('aria-hidden') === 'true') {
303
+ return true;
304
+ }
305
+ // Check computed style if available
306
+ try {
307
+ const style = getComputedStyle(element);
308
+ return style.display === 'none' || style.visibility === 'hidden';
309
+ }
310
+ catch (e) {
311
+ return false;
312
+ }
313
+ }
314
+ /**
315
+ * Check if a background image is likely decorative
316
+ * @param element Element to check
317
+ */
318
+ function isBackgroundLikelyDecorative(element) {
319
+ var _a, _b;
320
+ // Common decorative pattern elements
321
+ const decorativeElements = ['header', 'footer', 'section', 'article', 'div'];
322
+ const tagName = ((_a = element.tagName) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || '';
323
+ if (decorativeElements.includes(tagName)) {
324
+ // Check for certain classes that might indicate decorative backgrounds
325
+ const className = ((_b = element.className) === null || _b === void 0 ? void 0 : _b.toString()) || '';
326
+ if (className.includes('background') ||
327
+ className.includes('banner') ||
328
+ className.includes('hero') ||
329
+ className.includes('container') ||
330
+ className.includes('wrapper') ||
331
+ className.includes('section')) {
332
+ return true;
333
+ }
334
+ }
335
+ return false;
336
+ }
337
+ /**
338
+ * Determine if an image is likely decorative
339
+ * @param img Image to check
340
+ */
341
+ function isLikelyDecorativeImage(img) {
342
+ var _a;
343
+ // Images with role="presentation" are explicitly decorative
344
+ if (img.getAttribute('role') === 'presentation' || img.getAttribute('role') === 'none') {
345
+ return true;
346
+ }
347
+ // Very small images are often decorative
348
+ const width = parseInt(img.getAttribute('width') || img.width.toString());
349
+ const height = parseInt(img.getAttribute('height') || img.height.toString());
350
+ if ((width > 0 && width < 16) || (height > 0 && height < 16)) {
351
+ return true;
352
+ }
353
+ // Check class names for hints
354
+ const className = img.className || '';
355
+ if (className.includes('decoration') ||
356
+ className.includes('ornament') ||
357
+ className.includes('icon') ||
358
+ className.includes('separator') ||
359
+ className.includes('bg') ||
360
+ className.includes('background')) {
361
+ return true;
362
+ }
363
+ // Check if the image is inside a link with text
364
+ const parentLink = img.closest('a');
365
+ if (parentLink && ((_a = parentLink.textContent) === null || _a === void 0 ? void 0 : _a.trim()) && parentLink.textContent.trim().length > 0) {
366
+ return true;
367
+ }
368
+ return false;
369
+ }
370
+ /**
371
+ * Check if alt text appears to be generic or placeholder text
372
+ * @param altText Alt text to check
373
+ */
374
+ function hasGenericAltText(altText) {
375
+ const genericTerms = ['image', 'picture', 'photo', 'img', 'graphic', 'icon', 'logo', 'photo.jpg', 'untitled'];
376
+ const lowerAlt = altText.toLowerCase().trim();
377
+ // Check for file extensions in alt text
378
+ if (lowerAlt.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i)) {
379
+ return true;
380
+ }
381
+ // Check for generic terms used alone
382
+ if (genericTerms.includes(lowerAlt)) {
383
+ return true;
384
+ }
385
+ // Check for very short alt text that's not meaningful
386
+ if (lowerAlt.length < 5 && !['ok', 'yes', 'no'].includes(lowerAlt)) {
387
+ return true;
388
+ }
389
+ return false;
390
+ }
@@ -0,0 +1,15 @@
1
+ import { ScannerOptions, ScanResults } from '../types';
2
+ /**
3
+ * Accessibility checker for tab index, keyboard events, focus indicators, and interactive elements
4
+ */
5
+ declare const _default: {
6
+ /**
7
+ * Run accessibility checks on tab index, keyboard events, focus indicators, and interactive elements
8
+ * @param document DOM document
9
+ * @param window Browser window
10
+ * @param options Scanner options
11
+ * @returns Promise<ScanResults> Results from accessibility checks
12
+ */
13
+ check(document: Document, window: Window, options: ScannerOptions): Promise<ScanResults>;
14
+ };
15
+ export default _default;
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Accessibility checker for tab index, keyboard events, focus indicators, and interactive elements
5
+ */
6
+ exports.default = {
7
+ /**
8
+ * Run accessibility checks on tab index, keyboard events, focus indicators, and interactive elements
9
+ * @param document DOM document
10
+ * @param window Browser window
11
+ * @param options Scanner options
12
+ * @returns Promise<ScanResults> Results from accessibility checks
13
+ */
14
+ async check(document, window, options) {
15
+ const results = {
16
+ passes: [],
17
+ violations: [],
18
+ warnings: []
19
+ };
20
+ // Check tabindex values
21
+ checkTabindex(document, results);
22
+ // Check keyboard event handlers
23
+ checkKeyboardEvents(document, results);
24
+ // Check focus indicators
25
+ checkFocusIndicators(document, window, results);
26
+ // Check interactive elements
27
+ checkInteractiveElements(document, results);
28
+ return results;
29
+ }
30
+ };
31
+ /**
32
+ * Check for valid tabindex values
33
+ * @param document DOM document
34
+ * @param results Scan results
35
+ */
36
+ function checkTabindex(document, results) {
37
+ const elements = document.querySelectorAll('[tabindex]');
38
+ elements.forEach(element => {
39
+ const tabindex = element.getAttribute('tabindex');
40
+ const tabindexNum = parseInt(tabindex || '0');
41
+ const info = {
42
+ tagName: element.tagName.toLowerCase(),
43
+ id: element.id || null,
44
+ tabindex: tabindex || null
45
+ };
46
+ // Check for positive tabindex values (creates a custom tab order)
47
+ if (tabindexNum > 0) {
48
+ results.warnings.push({
49
+ rule: 'tabindex-positive',
50
+ element: info,
51
+ impact: 'moderate',
52
+ description: `Element has a positive tabindex value (${tabindexNum})`,
53
+ snippet: element.outerHTML,
54
+ wcag: ['2.4.3'],
55
+ help: 'Avoid positive tabindex values as they create a custom tab order'
56
+ });
57
+ }
58
+ // Check if non-interactive elements are focusable
59
+ const isNativelyFocusable = [
60
+ 'a', 'button', 'input', 'select', 'textarea', 'summary', 'details'
61
+ ].includes(element.tagName.toLowerCase());
62
+ const hasInteractiveRole = element.hasAttribute('role') && [
63
+ 'button', 'checkbox', 'link', 'menuitem', 'menuitemcheckbox',
64
+ 'menuitemradio', 'option', 'radio', 'slider', 'tab', 'textbox',
65
+ 'switch', 'searchbox', 'combobox'
66
+ ].includes(element.getAttribute('role') || '');
67
+ if (!isNativelyFocusable && !hasInteractiveRole && tabindexNum >= 0) {
68
+ results.warnings.push({
69
+ rule: 'tabindex-non-interactive',
70
+ element: info,
71
+ impact: 'moderate',
72
+ description: 'Non-interactive element has a tabindex making it focusable',
73
+ snippet: element.outerHTML,
74
+ wcag: ['2.1.1'],
75
+ help: 'Only make interactive elements focusable or add appropriate ARIA roles'
76
+ });
77
+ }
78
+ });
79
+ }
80
+ /**
81
+ * Check keyboard event handlers
82
+ * @param document DOM document
83
+ * @param results Scan results
84
+ */
85
+ function checkKeyboardEvents(document, results) {
86
+ // Elements with mouse event handlers but no keyboard equivalent
87
+ const allElements = document.querySelectorAll('*');
88
+ allElements.forEach(element => {
89
+ // This is a simple check - a full implementation would analyze event handlers
90
+ // through JavaScript frameworks, which is complex
91
+ const hasOnClick = element.hasAttribute('onclick');
92
+ const hasOnMousedown = element.hasAttribute('onmousedown');
93
+ const hasOnMouseup = element.hasAttribute('onmouseup');
94
+ const hasOnKeydown = element.hasAttribute('onkeydown');
95
+ const hasOnKeyup = element.hasAttribute('onkeyup');
96
+ const hasOnKeypress = element.hasAttribute('onkeypress');
97
+ const info = {
98
+ tagName: element.tagName.toLowerCase(),
99
+ id: element.id || null,
100
+ className: element.className || null
101
+ };
102
+ // Check if element has mouse event but no keyboard event
103
+ if ((hasOnClick || hasOnMousedown || hasOnMouseup) &&
104
+ !(hasOnKeydown || hasOnKeyup || hasOnKeypress)) {
105
+ // Skip natively clickable elements (browsers handle keyboard events)
106
+ if (['a', 'button', 'input', 'select', 'textarea'].includes(element.tagName.toLowerCase())) {
107
+ return;
108
+ }
109
+ results.warnings.push({
110
+ rule: 'keyboard-event-equivalents',
111
+ element: info,
112
+ impact: 'moderate',
113
+ description: 'Element has mouse event handlers but no keyboard event handlers',
114
+ snippet: element.outerHTML.slice(0, 150) + (element.outerHTML.length > 150 ? '...' : ''),
115
+ wcag: ['2.1.1'],
116
+ help: 'Ensure all functionality is operable through keyboard'
117
+ });
118
+ }
119
+ });
120
+ }
121
+ /**
122
+ * Check for focus indicators
123
+ * @param document DOM document
124
+ * @param window Browser window
125
+ * @param results Scan results
126
+ */
127
+ function checkFocusIndicators(document, window, results) {
128
+ // Elements that should have visible focus indicators
129
+ const focusableElements = document.querySelectorAll('a[href], button, input, textarea, select, [tabindex]:not([tabindex="-1"])');
130
+ focusableElements.forEach(element => {
131
+ // Without actually applying focus, we can't check the exact appearance
132
+ // But we can check for CSS that might suppress focus styles
133
+ const style = window.getComputedStyle(element);
134
+ const info = {
135
+ tagName: element.tagName.toLowerCase(),
136
+ id: element.id || null,
137
+ className: element.className || null
138
+ };
139
+ // Check for CSS that might remove the focus outline
140
+ if (style.outlineStyle === 'none' || style.outlineWidth === '0px') {
141
+ // Look for alternative focus indicators like background-color changes or borders
142
+ // This is a simplified check
143
+ results.warnings.push({
144
+ rule: 'focus-visible',
145
+ element: info,
146
+ impact: 'moderate',
147
+ description: 'Element may be missing visible focus indicator',
148
+ snippet: element.outerHTML.slice(0, 150) + (element.outerHTML.length > 150 ? '...' : ''),
149
+ wcag: ['2.4.7'],
150
+ help: 'Ensure all focusable elements have visible focus indicators'
151
+ });
152
+ }
153
+ });
154
+ }
155
+ /**
156
+ * Check interactive elements
157
+ * @param document DOM document
158
+ * @param results Scan results
159
+ */
160
+ function checkInteractiveElements(document, results) {
161
+ // Check for non-button elements that act as buttons
162
+ const clickableItems = document.querySelectorAll('div[onclick], span[onclick], a:not([href])');
163
+ clickableItems.forEach(element => {
164
+ const info = {
165
+ tagName: element.tagName.toLowerCase(),
166
+ id: element.id || null,
167
+ className: element.className || null
168
+ };
169
+ // Check for button role
170
+ const hasButtonRole = element.getAttribute('role') === 'button';
171
+ // Check for interactivity properties
172
+ const isInteractive = element.hasAttribute('tabindex') ||
173
+ element.hasAttribute('onclick') ||
174
+ element.hasAttribute('onkeyup') ||
175
+ element.hasAttribute('onkeydown');
176
+ if (isInteractive && !hasButtonRole && element.tagName.toLowerCase() !== 'button') {
177
+ results.violations.push({
178
+ rule: 'interactive-semantics',
179
+ element: info,
180
+ impact: 'serious',
181
+ description: 'Interactive element is missing semantic role',
182
+ snippet: element.outerHTML.slice(0, 150) + (element.outerHTML.length > 150 ? '...' : ''),
183
+ wcag: ['4.1.2'],
184
+ help: 'Add role="button" to non-button elements that act as buttons'
185
+ });
186
+ }
187
+ // Check if interactive elements are focusable
188
+ if (isInteractive && !element.hasAttribute('tabindex') && element.tagName.toLowerCase() !== 'a') {
189
+ results.violations.push({
190
+ rule: 'interactive-focusable',
191
+ element: info,
192
+ impact: 'serious',
193
+ description: 'Interactive element is not keyboard focusable',
194
+ snippet: element.outerHTML.slice(0, 150) + (element.outerHTML.length > 150 ? '...' : ''),
195
+ wcag: ['2.1.1'],
196
+ help: 'Add tabindex="0" to make interactive elements focusable'
197
+ });
198
+ }
199
+ });
200
+ // Check for links that open in new windows
201
+ const linksNewWindow = document.querySelectorAll('a[target="_blank"]');
202
+ linksNewWindow.forEach(link => {
203
+ var _a, _b;
204
+ const info = {
205
+ tagName: 'a',
206
+ id: link.id || null,
207
+ href: link.getAttribute('href') || null,
208
+ target: '_blank'
209
+ };
210
+ const hasWarning = ((_a = link.textContent) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes('new window')) ||
211
+ ((_b = link.textContent) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes('new tab')) ||
212
+ link.querySelector('svg, img[alt*="new window"], img[alt*="external"]');
213
+ if (!hasWarning) {
214
+ results.warnings.push({
215
+ rule: 'link-new-window',
216
+ element: info,
217
+ impact: 'moderate',
218
+ description: 'Link opens in new window without warning',
219
+ snippet: link.outerHTML,
220
+ wcag: ['3.2.2'],
221
+ help: 'Indicate in the link text that it opens in a new window'
222
+ });
223
+ }
224
+ });
225
+ }
@@ -0,0 +1,15 @@
1
+ import { ScannerOptions, ScanResults } from '../types';
2
+ /**
3
+ * Check accessibility of structural elements (headings, landmarks, etc.)
4
+ */
5
+ declare const _default: {
6
+ /**
7
+ * Run accessibility checks on the entire document
8
+ * @param document DOM document
9
+ * @param window Browser window
10
+ * @param options Scanner options
11
+ * @returns Promise<ScanResults> Results from accessibility checks
12
+ */
13
+ check(document: Document, window: Window, options: ScannerOptions): Promise<ScanResults>;
14
+ };
15
+ export default _default;