@govtechsg/oobee 0.10.36 → 0.10.39

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.
Files changed (33) hide show
  1. package/DETAILS.md +3 -3
  2. package/INTEGRATION.md +142 -53
  3. package/README.md +15 -0
  4. package/exclusions.txt +4 -1
  5. package/package.json +2 -2
  6. package/src/constants/cliFunctions.ts +0 -7
  7. package/src/constants/common.ts +39 -1
  8. package/src/constants/constants.ts +9 -8
  9. package/src/crawlers/commonCrawlerFunc.ts +66 -219
  10. package/src/crawlers/crawlDomain.ts +6 -2
  11. package/src/crawlers/crawlLocalFile.ts +2 -0
  12. package/src/crawlers/crawlSitemap.ts +5 -3
  13. package/src/crawlers/custom/escapeCssSelector.ts +10 -0
  14. package/src/crawlers/custom/evaluateAltText.ts +13 -0
  15. package/src/crawlers/custom/extractAndGradeText.ts +0 -2
  16. package/src/crawlers/custom/extractText.ts +28 -0
  17. package/src/crawlers/custom/findElementByCssSelector.ts +46 -0
  18. package/src/crawlers/custom/flagUnlabelledClickableElements.ts +1006 -901
  19. package/src/crawlers/custom/framesCheck.ts +51 -0
  20. package/src/crawlers/custom/getAxeConfiguration.ts +126 -0
  21. package/src/crawlers/custom/gradeReadability.ts +30 -0
  22. package/src/crawlers/custom/xPathToCss.ts +178 -0
  23. package/src/mergeAxeResults.ts +467 -129
  24. package/src/npmIndex.ts +130 -62
  25. package/src/static/ejs/partials/components/ruleOffcanvas.ejs +1 -1
  26. package/src/static/ejs/partials/components/scanAbout.ejs +1 -1
  27. package/src/static/ejs/partials/footer.ejs +3 -3
  28. package/src/static/ejs/partials/scripts/reportSearch.ejs +112 -74
  29. package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +2 -2
  30. package/src/static/ejs/partials/summaryMain.ejs +3 -3
  31. package/src/static/ejs/report.ejs +3 -3
  32. package/src/xPathToCssCypress.ts +178 -0
  33. package/src/crawlers/customAxeFunctions.ts +0 -82
@@ -1,1036 +1,1141 @@
1
- import { Page } from 'playwright';
2
-
3
- export const flagUnlabelledClickableElements = async (page: Page) => {
1
+ export async function flagUnlabelledClickableElements() {
4
2
  // Just paste the entire script into the body of the page.evaluate callback below
5
3
  // There's some code that is not needed when running this on backend but
6
4
  // we avoid changing the script for now to make it easy to update
7
- return await page.evaluate(() => {
8
- const allowNonClickableFlagging = true; // Change this to true to flag non-clickable images
9
- const landmarkElements = [
10
- 'header',
11
- 'footer',
12
- 'nav',
13
- 'main',
14
- 'article',
15
- 'section',
16
- 'aside',
17
- 'form',
18
- ];
19
- const loggingEnabled = false; // Set to true to enable console warnings
20
-
21
- let previousFlaggedXPathsByDocument = {}; // Object to hold previous flagged XPaths
22
- const previousAllFlaggedElementsXPaths = []; // Array to store all flagged XPaths
23
-
24
- function getXPath(element: Node) {
25
- if (!element) return null;
26
- if (element instanceof HTMLElement && element.id) {
27
- return `//*[@id="${element.id}"]`;
28
- }
29
- if (element === element.ownerDocument.body) {
30
- return '/html/body';
31
- }
32
- if (!element.parentNode || element.parentNode.nodeType !== 1) {
33
- return '';
5
+ const allowNonClickableFlagging = true; // Change this to true to flag non-clickable images
6
+ const landmarkElements = [
7
+ 'header',
8
+ 'footer',
9
+ 'nav',
10
+ 'main',
11
+ 'article',
12
+ 'section',
13
+ 'aside',
14
+ 'form',
15
+ ];
16
+ const loggingEnabled = false; // Set to true to enable console warnings
17
+
18
+ let previousFlaggedXPathsByDocument = {}; // Object to hold previous flagged XPaths
19
+ const previousAllFlaggedElementsXPaths = []; // Array to store all flagged XPaths
20
+
21
+ function getXPath(element: Node) {
22
+ if (!element) return null;
23
+ if (element instanceof HTMLElement && element.id) {
24
+ return `//*[@id="${element.id}"]`;
25
+ }
26
+ if (element === element.ownerDocument.body) {
27
+ return '/html/body';
28
+ }
29
+ if (!element.parentNode || element.parentNode.nodeType !== 1) {
30
+ return '';
31
+ }
32
+
33
+ const siblings: Node[] = Array.from(element.parentNode.childNodes).filter(
34
+ node => node.nodeName === element.nodeName,
35
+ );
36
+ const ix = siblings.indexOf(element) + 1;
37
+ const siblingIndex = siblings.length > 1 ? `[${ix}]` : '';
38
+ return `${getXPath(element.parentNode)}/${element.nodeName.toLowerCase()}${siblingIndex}`;
39
+ }
40
+
41
+ function customConsoleWarn(message: string, data?: any) {
42
+ if (loggingEnabled) {
43
+ if (data) {
44
+ console.warn(message, data);
45
+ } else {
46
+ console.warn(message);
34
47
  }
48
+ }
49
+ }
35
50
 
36
- const siblings: Node[] = Array.from(element.parentNode.childNodes).filter(
37
- node => node.nodeName === element.nodeName,
38
- );
39
- const ix = siblings.indexOf(element) + 1;
40
- const siblingIndex = siblings.length > 1 ? `[${ix}]` : '';
41
- return `${getXPath(element.parentNode)}/${element.nodeName.toLowerCase()}${siblingIndex}`;
51
+ function hasPointerCursor(element: Element) {
52
+ const computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
53
+ const hasPointerStyle = computedStyle.cursor === 'pointer';
54
+ const hasOnClick = element.hasAttribute('onclick');
55
+ const hasEventListeners = Object.keys(element).some(prop => prop.startsWith('on'));
56
+
57
+ // Check if the element is inherently interactive
58
+ const isClickableRole = ['button', 'link', 'menuitem'].includes(element.getAttribute('role'));
59
+ const isNativeClickableElement =
60
+ ['a', 'button', 'input'].includes(element.nodeName.toLowerCase()) &&
61
+ (element.nodeName.toLowerCase() !== 'a' || element.hasAttribute('href'));
62
+ const hasTabIndex =
63
+ element.hasAttribute('tabindex') && element.getAttribute('tabindex') !== '-1';
64
+
65
+ return (
66
+ hasPointerStyle ||
67
+ hasOnClick ||
68
+ hasEventListeners ||
69
+ isClickableRole ||
70
+ isNativeClickableElement ||
71
+ hasTabIndex
72
+ );
73
+ }
74
+
75
+ function isAccessibleText(value: string) {
76
+ if (!value || value.trim().length === 0) {
77
+ return false;
42
78
  }
43
79
 
44
- function customConsoleWarn(message: string, data?: any) {
45
- if (loggingEnabled) {
46
- if (data) {
47
- console.warn(message, data);
48
- } else {
49
- console.warn(message);
50
- }
51
- }
80
+ const trimmedValue = value.trim();
81
+
82
+ // Check if the text contains any private use characters
83
+ const privateUseRegex = /\p{Private_Use}/u;
84
+ if (privateUseRegex.test(trimmedValue)) {
85
+ return false;
52
86
  }
53
87
 
54
- function hasPointerCursor(element: Element) {
55
- const computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
56
- const hasPointerStyle = computedStyle.cursor === 'pointer';
57
- const hasOnClick = element.hasAttribute('onclick');
58
- const hasEventListeners = Object.keys(element).some(prop => prop.startsWith('on'));
88
+ // Check if the text contains at least one letter or number
89
+ const accessibleTextRegex = /[\p{L}\p{N}]/u;
90
+ if (accessibleTextRegex.test(trimmedValue)) {
91
+ return true;
92
+ }
59
93
 
60
- // Check if the element is inherently interactive
61
- const isClickableRole = ['button', 'link', 'menuitem'].includes(element.getAttribute('role'));
62
- const isNativeClickableElement =
63
- ['a', 'button', 'input'].includes(element.nodeName.toLowerCase()) &&
64
- (element.nodeName.toLowerCase() !== 'a' || element.hasAttribute('href'));
65
- const hasTabIndex =
66
- element.hasAttribute('tabindex') && element.getAttribute('tabindex') !== '-1';
94
+ // If it doesn't contain letters or numbers, consider it not accessible
95
+ return false;
96
+ }
67
97
 
98
+ function isInOpenDetails(element: Element) {
99
+ let parentDetails = element.closest('details');
100
+ return parentDetails ? parentDetails.open : true;
101
+ }
102
+
103
+ function isVisibleFocusAble(el: Element) {
104
+ if (!el) {
105
+ return false;
106
+ }
107
+ if (el.nodeName !== undefined && el.nodeName === '#text') {
108
+ // cause #text cannot getComputedStyle
109
+ return false;
110
+ }
111
+ try {
112
+ const style = window.getComputedStyle(el);
113
+ const rect = el.getBoundingClientRect();
68
114
  return (
69
- hasPointerStyle ||
70
- hasOnClick ||
71
- hasEventListeners ||
72
- isClickableRole ||
73
- isNativeClickableElement ||
74
- hasTabIndex
115
+ // Visible
116
+ style.display !== 'none' &&
117
+ style.visibility !== 'hidden' &&
118
+ style.opacity !== '0' &&
119
+ rect.width > 0 &&
120
+ rect.height > 0 &&
121
+ // <detail> tag will show it as visual so need to account for that
122
+ isInOpenDetails(el)
75
123
  );
124
+ } catch (error) {
125
+ console.log('Error in ELEMENT', el, error.message);
126
+ return false;
127
+ }
128
+ }
129
+
130
+ function isValidUnicode(text: string) {
131
+ if (typeof text !== 'string') {
132
+ return false;
76
133
  }
77
134
 
78
- function isAccessibleText(value: string) {
79
- if (!value || value.trim().length === 0) {
80
- return false;
135
+ // Regular expression to match valid Unicode characters, including surrogate pairs
136
+ const validTextOrEmojiRegex = /[\p{L}\p{N}\p{S}\p{P}\p{Emoji}]/gu; // Letters, numbers, symbols, punctuation, and emojis
137
+
138
+ // Check if the text contains at least one valid character or emoji
139
+ return validTextOrEmojiRegex.test(text);
140
+ }
141
+
142
+ function getElementById(element: Element, id: string) {
143
+ return element.ownerDocument.getElementById(id);
144
+ }
145
+
146
+ function getAriaLabelledByText(element: Element) {
147
+ const labelledById = element.getAttribute('aria-labelledby');
148
+ if (labelledById) {
149
+ const labelledByElement = getElementById(element, labelledById);
150
+ if (labelledByElement) {
151
+ const ariaLabel = labelledByElement.getAttribute('aria-label');
152
+ return ariaLabel ? ariaLabel.trim() : labelledByElement.textContent.trim();
81
153
  }
154
+ }
155
+ return '';
156
+ }
157
+
158
+ function hasAccessibleLabel(element: Element) {
159
+ const ariaLabel = element.getAttribute('aria-label');
160
+ const ariaLabelledByText = getAriaLabelledByText(element);
161
+ const altText = element.getAttribute('alt');
162
+ const title = element.getAttribute('title');
163
+
164
+ return (
165
+ isAccessibleText(ariaLabel) ||
166
+ isAccessibleText(ariaLabelledByText) ||
167
+ isAccessibleText(altText) ||
168
+ isAccessibleText(title)
169
+ );
170
+ }
82
171
 
83
- const trimmedValue = value.trim();
172
+ function hasSummaryOrDetailsLabel(element: Element) {
173
+ const summary = element.closest('summary, details');
174
+ return summary && hasAccessibleLabel(summary);
175
+ }
84
176
 
85
- // Check if the text contains any private use characters
86
- const privateUseRegex = /\p{Private_Use}/u;
87
- if (privateUseRegex.test(trimmedValue)) {
88
- return false;
177
+ function hasSiblingWithAccessibleLabel(element: Element) {
178
+ // Check all siblings (previous and next)
179
+ let sibling = element.previousElementSibling;
180
+ while (sibling) {
181
+ if (hasAccessibleLabel(sibling)) {
182
+ return true;
89
183
  }
184
+ sibling = sibling.previousElementSibling;
185
+ }
90
186
 
91
- // Check if the text contains at least one letter or number
92
- const accessibleTextRegex = /[\p{L}\p{N}]/u;
93
- if (accessibleTextRegex.test(trimmedValue)) {
187
+ sibling = element.nextElementSibling;
188
+ while (sibling) {
189
+ if (hasAccessibleLabel(sibling)) {
94
190
  return true;
95
191
  }
192
+ sibling = sibling.nextElementSibling;
193
+ }
194
+
195
+ return false;
196
+ }
197
+
198
+ function hasSiblingOrParentAccessibleLabel(element: Element) {
199
+ // Check previous and next siblings
200
+ const previousSibling = element.previousElementSibling;
201
+ const nextSibling = element.nextElementSibling;
202
+ if (
203
+ (previousSibling && hasAccessibleLabel(previousSibling)) ||
204
+ (nextSibling && hasAccessibleLabel(nextSibling))
205
+ ) {
206
+ return true;
207
+ }
208
+
209
+ // Check the parent element
210
+ const parent = element.parentElement;
211
+ if (parent && hasAccessibleLabel(parent)) {
212
+ return true;
213
+ }
96
214
 
97
- // If it doesn't contain letters or numbers, consider it not accessible
215
+ return false;
216
+ }
217
+
218
+ function hasChildWithAccessibleText(element: Element) {
219
+ // Check element children
220
+ const hasAccessibleChildElement = Array.from(element.children).some(child => {
221
+ if (child.nodeName.toLowerCase() === 'style' || child.nodeName.toLowerCase() === 'script') {
222
+ return false;
223
+ }
224
+ // Skip children that are aria-hidden
225
+ if (child.getAttribute('aria-hidden') === 'true') {
226
+ return false;
227
+ }
228
+ return (
229
+ isAccessibleText(child.textContent) || hasAccessibleLabel(child) || hasCSSContent(child)
230
+ );
231
+ });
232
+
233
+ // Check direct text nodes
234
+ const hasDirectAccessibleText = Array.from(element.childNodes).some(node => {
235
+ if (node.nodeType === Node.TEXT_NODE) {
236
+ return isAccessibleText(node.textContent);
237
+ }
98
238
  return false;
239
+ });
240
+
241
+ return hasAccessibleChildElement || hasDirectAccessibleText;
242
+ }
243
+
244
+ function hasAllChildrenAccessible(element: Element) {
245
+ // If the element is aria-hidden, consider it accessible
246
+ if (element.getAttribute('aria-hidden') === 'true') {
247
+ return true;
99
248
  }
100
249
 
101
- function isInOpenDetails(element:Element) {
102
- let parentDetails = element.closest('details');
103
- return parentDetails ? parentDetails.open : true;
250
+ // Check if the element itself has an accessible label, text content, or CSS content
251
+ if (
252
+ hasAccessibleLabel(element) ||
253
+ isAccessibleText(element.textContent) ||
254
+ hasCSSContent(element)
255
+ ) {
256
+ return true;
104
257
  }
105
258
 
106
- function isVisibleFocusAble(el:Element) {
107
- if (!el)
108
- {
109
- return false;
259
+ // If the element has children, ensure at least one of them is accessible
260
+ if (element.children.length > 0) {
261
+ return Array.from(element.children).some(child => {
262
+ // If child is aria-hidden, skip it in the accessibility check
263
+ if (child.getAttribute('aria-hidden') === 'true') {
264
+ return true;
265
+ }
266
+ // Recursively check if the child or any of its descendants are accessible
267
+ return hasAllChildrenAccessible(child);
268
+ });
269
+ }
270
+
271
+ // If the element and all its children have no accessible labels or text, it's not accessible
272
+ return false;
273
+ }
274
+
275
+ function hasChildNotANewInteractWithAccessibleText(element: Element) {
276
+ // Helper function to check if the element is a link or button
277
+ const isLinkOrButton = (child: Node) => {
278
+ if (child instanceof Element) {
279
+ // Check if the child is an Element
280
+ return (
281
+ child.nodeName.toLowerCase() === 'a' ||
282
+ child.nodeName.toLowerCase() === 'button' ||
283
+ child.getAttribute('role') === 'link' ||
284
+ child.getAttribute('role') === 'button'
285
+ );
110
286
  }
111
- if (el.nodeName !== undefined && el.nodeName === "#text") // cause #text cannot getComputedStyle
112
- {
287
+ return false;
288
+ };
289
+
290
+ // Check element children
291
+ const hasAccessibleChildElement = Array.from(element.children).some(child => {
292
+ if (child instanceof Element) {
293
+ // Ensure child is an Element
294
+ if (!hasPointerCursor(child)) {
113
295
  return false;
114
- }
115
- try {
116
- const style = window.getComputedStyle(el);
117
- const rect = el.getBoundingClientRect();
118
- return (
119
- // Visible
120
- style.display !== 'none'
121
- && style.visibility !== 'hidden'
122
- && style.opacity !== "0"
123
- && rect.width > 0 && rect.height > 0
124
- // <detail> tag will show it as visual so need to account for that
125
- && isInOpenDetails(el)
126
- );
127
- } catch (error) {
128
- console.log("Error in ELEMENT",el,error.message)
296
+ }
297
+
298
+ if (child.nodeName.toLowerCase() === 'style' || child.nodeName.toLowerCase() === 'script') {
129
299
  return false;
300
+ }
301
+
302
+ // Skip children that are aria-hidden or links/buttons
303
+ if (
304
+ child.getAttribute('aria-hidden') === 'true' ||
305
+ isLinkOrButton(child) ||
306
+ !isVisibleFocusAble(child)
307
+ ) {
308
+ return false;
309
+ }
310
+
311
+ // Check if the child element has accessible text or label
312
+ return (
313
+ isAccessibleText(getTextContent(child)) ||
314
+ hasAccessibleLabel(child) ||
315
+ hasCSSContent(child)
316
+ );
130
317
  }
131
- };
318
+ return false;
319
+ });
320
+
321
+ // Check direct text nodes inside the element itself (like <a>"text"</a>)
322
+ const hasDirectAccessibleText = Array.from(element.childNodes).some(node => {
323
+ if (node.nodeType === Node.TEXT_NODE) {
324
+ const textContent = getTextContent(node);
132
325
 
133
- function isValidUnicode(text :string) {
134
- if (typeof text !== "string") {
326
+ // Check if the text contains non-ASCII characters (Unicode)
327
+ const containsUnicode = /[^\x00-\x7F]/.test(textContent);
328
+
329
+ // If contains non-ASCII characters, validate with isValidUnicode
330
+ if (containsUnicode) {
331
+ return isValidUnicode(textContent);
332
+ }
333
+
334
+ // Otherwise, just check if it's non-empty text
335
+ return textContent.length > 0;
336
+ }
337
+
338
+ // Recursively check for text content inside child nodes of elements that are not links or buttons
339
+ if (node.nodeType === Node.ELEMENT_NODE && node instanceof Element && !isLinkOrButton(node)) {
340
+ return Array.from(node.childNodes).some(innerNode => {
341
+ if (innerNode.nodeType === Node.TEXT_NODE) {
342
+ const innerTextContent = getTextContent(innerNode).trim();
343
+ return innerTextContent && !isValidUnicode(innerTextContent); // Check for non-Unicode content
344
+ }
135
345
  return false;
346
+ });
136
347
  }
137
-
138
- // Regular expression to match valid Unicode characters, including surrogate pairs
139
- const validTextOrEmojiRegex = /[\p{L}\p{N}\p{S}\p{P}\p{Emoji}]/gu; // Letters, numbers, symbols, punctuation, and emojis
140
-
141
- // Check if the text contains at least one valid character or emoji
142
- return validTextOrEmojiRegex.test(text);
348
+
349
+ return false;
350
+ });
351
+
352
+ return hasAccessibleChildElement || hasDirectAccessibleText;
143
353
  }
144
354
 
145
- function getElementById(element: Element, id: string) {
146
- return element.ownerDocument.getElementById(id);
355
+ const style = document.createElement('style');
356
+ style.innerHTML = `
357
+ .highlight-flagged {
358
+ outline: 4px solid rgba(128, 0, 128, 1) !important; /* Thicker primary outline with purple in rgba format */
359
+ box-shadow:
360
+ 0 0 25px 15px rgba(255, 255, 255, 1), /* White glow for contrast */
361
+ 0 0 15px 10px rgba(144, 33, 166, 1) !important; /* Consistent purple glow in rgba format */
147
362
  }
148
-
149
- function getAriaLabelledByText(element: Element) {
150
- const labelledById = element.getAttribute('aria-labelledby');
151
- if (labelledById) {
152
- const labelledByElement = getElementById(element, labelledById);
153
- if (labelledByElement) {
154
- const ariaLabel = labelledByElement.getAttribute('aria-label');
155
- return ariaLabel ? ariaLabel.trim() : labelledByElement.textContent.trim();
156
- }
363
+ `;
364
+ document.head.appendChild(style);
365
+
366
+ function injectStylesIntoFrame(frame: HTMLIFrameElement) {
367
+ try {
368
+ const frameDocument = frame.contentDocument || frame.contentWindow.document;
369
+ if (frameDocument) {
370
+ const frameStyle = frameDocument.createElement('style');
371
+ frameStyle.innerHTML = `
372
+ .highlight-flagged {
373
+ outline: 4px solid rgba(128, 0, 128, 1) !important; /* Thicker primary outline with purple in rgba format */
374
+ box-shadow:
375
+ 0 0 25px 15px rgba(255, 255, 255, 1), /* White glow for contrast */
376
+ 0 0 15px 10px rgba(144, 33, 166, 1) !important; /* Consistent purple glow in rgba format */
377
+ }
378
+ `;
379
+ frameDocument.head.appendChild(frameStyle);
157
380
  }
158
- return '';
381
+ } catch (error) {
382
+ customConsoleWarn(`Cannot access frame document: ${error}`);
159
383
  }
384
+ }
160
385
 
161
- function hasAccessibleLabel(element: Element) {
162
- const ariaLabel = element.getAttribute('aria-label');
163
- const ariaLabelledByText = getAriaLabelledByText(element);
164
- const altText = element.getAttribute('alt');
165
- const title = element.getAttribute('title');
386
+ function hasCSSContent(element: Element) {
387
+ const beforeContent = window.getComputedStyle(element, '::before').getPropertyValue('content');
388
+ const afterContent = window.getComputedStyle(element, '::after').getPropertyValue('content');
166
389
 
167
- return (
168
- isAccessibleText(ariaLabel) ||
169
- isAccessibleText(ariaLabelledByText) ||
170
- isAccessibleText(altText) ||
171
- isAccessibleText(title)
172
- );
390
+ function isAccessibleContent(value: string) {
391
+ if (!value || value === 'none' || value === 'normal') {
392
+ return false;
393
+ }
394
+ // Remove quotes from the content value
395
+ const unquotedValue = value.replace(/^['"]|['"]$/g, '').trim();
396
+
397
+ // Use the isAccessibleText function
398
+ return isAccessibleText(unquotedValue);
173
399
  }
174
400
 
175
- function hasSummaryOrDetailsLabel(element: Element) {
176
- const summary = element.closest('summary, details');
177
- return summary && hasAccessibleLabel(summary);
401
+ return isAccessibleContent(beforeContent) || isAccessibleContent(afterContent);
402
+ }
403
+
404
+ function isElementTooSmall(element: Element) {
405
+ // Get the bounding rectangle of the element
406
+ const rect = element.getBoundingClientRect();
407
+
408
+ // Check if width or height is less than 1
409
+ return rect.width < 1 || rect.height < 1;
410
+ }
411
+
412
+ function getTextContent(element: Element | ChildNode): string {
413
+ if (element.nodeType === Node.TEXT_NODE) {
414
+ return element.nodeValue?.trim() ?? ''; // Return the text directly if it's a TEXT_NODE
178
415
  }
179
416
 
180
- function hasSiblingWithAccessibleLabel(element: Element) {
181
- // Check all siblings (previous and next)
182
- let sibling = element.previousElementSibling;
183
- while (sibling) {
184
- if (hasAccessibleLabel(sibling)) {
185
- return true;
417
+ let textContent = '';
418
+
419
+ for (const node of element.childNodes) {
420
+ if (node.nodeType === Node.TEXT_NODE) {
421
+ textContent += node.nodeValue?.trim() ?? ''; // Append text content from text nodes
422
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
423
+ // Type assertion: node is an Element
424
+ const elementNode = node as Element;
425
+
426
+ // If it's an SVG and has a <title> tag inside it, we want to grab that text
427
+ if (elementNode.tagName.toLowerCase() === 'svg') {
428
+ const titleElement = elementNode.querySelector('title');
429
+ if (titleElement && isVisibleFocusAble(elementNode)) {
430
+ return titleElement.textContent?.trim() ?? ''; // Return the title text if valid
431
+ }
186
432
  }
187
- sibling = sibling.previousElementSibling;
188
- }
189
433
 
190
- sibling = element.nextElementSibling;
191
- while (sibling) {
192
- if (hasAccessibleLabel(sibling)) {
193
- return true;
434
+ // Recursively check child elements if it's an element node
435
+ if (isVisibleFocusAble(elementNode)) {
436
+ const childText = getTextContent(elementNode);
437
+ if (childText) {
438
+ textContent += childText; // Append valid child text
439
+ }
194
440
  }
195
- sibling = sibling.nextElementSibling;
196
441
  }
442
+ }
443
+
444
+ return textContent.trim(); // Return the combined text content
445
+ }
446
+
447
+ function shouldFlagElement(element: HTMLElement, allowNonClickableFlagging: boolean) {
448
+ if (isElementTooSmall(element)) {
449
+ return false;
450
+ }
451
+
452
+ // Skip non-clickable elements if allowNonClickableFlagging is false
453
+ if (allowNonClickableFlagging && !hasPointerCursor(element)) {
454
+ customConsoleWarn(
455
+ 'Element is not clickable and allowNonClickableFlagging is false, skipping flagging.',
456
+ );
457
+ return false;
458
+ }
197
459
 
460
+ // Do not flag elements if any ancestor has aria-hidden="true"
461
+ if (element.closest('[aria-hidden="true"]')) {
462
+ customConsoleWarn("An ancestor element has aria-hidden='true', skipping flagging.");
198
463
  return false;
199
464
  }
200
465
 
201
- function hasSiblingOrParentAccessibleLabel(element: Element) {
202
- // Check previous and next siblings
203
- const previousSibling = element.previousElementSibling;
204
- const nextSibling = element.nextElementSibling;
466
+ let parents = element.parentElement;
467
+
468
+ // Causing false negative of svg
469
+ if (parents) {
470
+ // Check if the parent has an accessible label
471
+ if (hasAccessibleLabel(parents) || hasChildNotANewInteractWithAccessibleText(parents)) {
472
+ customConsoleWarn(
473
+ 'Parent element has an accessible label, skipping flagging of this element.',
474
+ );
475
+ return false;
476
+ }
477
+ }
478
+
479
+ let maxLayers = 3;
480
+ let tracedBackedLayerCount = 0;
481
+ while (parents && tracedBackedLayerCount <= maxLayers) {
482
+ // Skip flagging if the parent or the element itself has an accessible label
483
+ if (hasAccessibleLabel(parents) || hasChildNotANewInteractWithAccessibleText(parents)) {
484
+ customConsoleWarn('Parent or element has an accessible label, skipping flagging.', parents);
485
+ return false;
486
+ }
487
+
488
+ // Skip flagging if the parent is a button-like element with aria-expanded
205
489
  if (
206
- (previousSibling && hasAccessibleLabel(previousSibling)) ||
207
- (nextSibling && hasAccessibleLabel(nextSibling))
490
+ parents.getAttribute('role') === 'button' &&
491
+ (parents.hasAttribute('aria-expanded') || parents.hasAttribute('aria-controls'))
208
492
  ) {
209
- return true;
493
+ customConsoleWarn(
494
+ 'Parent element is an interactive button with aria-expanded or aria-controls, skipping flagging.',
495
+ );
496
+ return false;
210
497
  }
211
498
 
212
- // Check the parent element
213
- const parent = element.parentElement;
214
- if (parent && hasAccessibleLabel(parent)) {
215
- return true;
499
+ // Skip flagging if an ancestor has an accessible label or an interactive role (e.g., button, link)
500
+ if (
501
+ ['div', 'section', 'article', 'nav'].includes(parents.nodeName.toLowerCase()) &&
502
+ hasAccessibleLabel(parents)
503
+ ) {
504
+ customConsoleWarn(
505
+ 'Ancestor element with contextual role has an accessible label, skipping flagging.',
506
+ );
507
+ return false;
508
+ }
509
+
510
+ // Skip flag if parent is an a link or button that already contains accessible text
511
+ if (
512
+ (parents.nodeName.toLowerCase() === 'a' ||
513
+ parents.nodeName.toLowerCase() === 'button' ||
514
+ parents.getAttribute('role') === 'link' ||
515
+ parents.getAttribute('role') === 'button') &&
516
+ hasChildWithAccessibleText(parents)
517
+ ) {
518
+ return false;
216
519
  }
217
520
 
521
+ parents = parents.parentElement;
522
+ tracedBackedLayerCount++;
523
+ }
524
+
525
+ // Skip elements with role="menuitem" if an accessible sibling, parent, or child is present
526
+ if (element.getAttribute('role') === 'menuitem') {
527
+ if (
528
+ hasSiblingWithAccessibleLabel(element) ||
529
+ hasChildWithAccessibleText(element) ||
530
+ hasAccessibleLabel(element.parentElement)
531
+ ) {
532
+ customConsoleWarn(
533
+ 'Menuitem element or its sibling/parent has an accessible label, skipping flagging.',
534
+ );
535
+ return false;
536
+ }
537
+ }
538
+
539
+ // Skip flagging child elements if the parent element has role="menuitem" and is accessible
540
+ const parentMenuItem = element.closest('[role="menuitem"]');
541
+ if (
542
+ parentMenuItem &&
543
+ (hasAccessibleLabel(parentMenuItem) || hasChildWithAccessibleText(parentMenuItem))
544
+ ) {
545
+ customConsoleWarn(
546
+ 'Parent menuitem element has an accessible label or child with accessible text, skipping flagging of its children.',
547
+ );
218
548
  return false;
219
549
  }
220
550
 
221
- function hasChildWithAccessibleText(element: Element) {
222
- // Check element children
223
- const hasAccessibleChildElement = Array.from(element.children).some(child => {
224
- if (child.nodeName.toLowerCase() === 'style' || child.nodeName.toLowerCase() === 'script') {
551
+ // Add the new condition for empty div or span elements without any accessible text or children with accessible labels
552
+ if (
553
+ (element.nodeName.toLowerCase() === 'span' || element.nodeName.toLowerCase() === 'div') &&
554
+ element.children.length === 0 &&
555
+ getTextContent(element).trim().length === 0
556
+ ) {
557
+ const parent = element.parentElement;
558
+ if (parent) {
559
+ const hasAccessibleChild = Array.from(parent.children).some(
560
+ child => child !== element && hasAccessibleLabel(child),
561
+ );
562
+
563
+ if (hasAccessibleChild) {
564
+ customConsoleWarn(
565
+ 'Parent element has an accessible child, skipping flagging of empty span or div.',
566
+ );
225
567
  return false;
226
568
  }
227
- // Skip children that are aria-hidden
228
- if (child.getAttribute('aria-hidden') === 'true') {
229
- return false;
569
+ }
570
+ }
571
+
572
+ // Do not flag elements with aria-hidden="true"
573
+ if (element.getAttribute('aria-hidden') === 'true') {
574
+ customConsoleWarn('Element is aria-hidden, skipping flagging.');
575
+ return false;
576
+ }
577
+
578
+ if (
579
+ element.getAttribute('aria-labelledby') !== null &&
580
+ element.getAttribute('aria-labelledby') !== ''
581
+ ) {
582
+ // Get the list of IDs referenced in aria-labelledby
583
+ const ids = element.getAttribute('aria-labelledby').split(' ');
584
+ let shouldNotFlag = false;
585
+
586
+ // Loop through each ID and find the corresponding elements
587
+ ids.forEach(id => {
588
+ const referencedElement = document.getElementById(id);
589
+
590
+ // Check if the element was found
591
+ if (
592
+ referencedElement &&
593
+ (hasAccessibleLabel(referencedElement) ||
594
+ isAccessibleText(getTextContent(referencedElement)) ||
595
+ hasAllChildrenAccessible(referencedElement))
596
+ ) {
597
+ shouldNotFlag = true;
230
598
  }
231
- return (
232
- isAccessibleText(child.textContent) || hasAccessibleLabel(child) || hasCSSContent(child)
233
- );
234
599
  });
235
600
 
236
- // Check direct text nodes
237
- const hasDirectAccessibleText = Array.from(element.childNodes).some(node => {
238
- if (node.nodeType === Node.TEXT_NODE) {
239
- return isAccessibleText(node.textContent);
240
- }
601
+ if (shouldNotFlag) {
241
602
  return false;
242
- });
603
+ }
604
+ }
243
605
 
244
- return hasAccessibleChildElement || hasDirectAccessibleText;
606
+ // Do not flag elements with role="presentation"
607
+ if (element.getAttribute('role') === 'presentation') {
608
+ customConsoleWarn("Element has role='presentation', skipping flagging.");
609
+ return false;
245
610
  }
246
611
 
247
- function hasAllChildrenAccessible(element: Element) {
248
- // If the element is aria-hidden, consider it accessible
249
- if (element.getAttribute('aria-hidden') === 'true') {
250
- return true;
612
+ if (element.dataset.flagged === 'true') {
613
+ customConsoleWarn('Element is already flagged.');
614
+ return false;
615
+ }
616
+
617
+ // If an ancestor element is flagged, do not flag this element
618
+ if (element.closest('[data-flagged="true"]')) {
619
+ customConsoleWarn('An ancestor element is already flagged.');
620
+ return false;
621
+ }
622
+
623
+ // Skip elements that are not visible (e.g., display:none)
624
+ const computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
625
+ if (
626
+ computedStyle.display === 'none' ||
627
+ computedStyle.visibility === 'hidden' ||
628
+ element.offsetParent === null
629
+ ) {
630
+ customConsoleWarn('Element is not visible, skipping flagging.');
631
+ return false;
632
+ }
633
+
634
+ // Skip empty <div> or <span> elements without any accessible text or children with accessible labels, unless they have a pointer cursor
635
+ if (
636
+ (element.nodeName.toLowerCase() === 'div' || element.nodeName.toLowerCase() === 'span') &&
637
+ element.children.length === 0 &&
638
+ getTextContent(element).trim().length === 0
639
+ ) {
640
+ if (!hasPointerCursor(element)) {
641
+ customConsoleWarn(
642
+ 'Empty div or span without accessible text and without pointer cursor, skipping flagging.',
643
+ );
644
+ return false;
645
+ }
646
+
647
+ // **New background-image check**
648
+ const backgroundImage = window.getComputedStyle(element).getPropertyValue('background-image');
649
+ if (backgroundImage && backgroundImage !== 'none') {
650
+ customConsoleWarn('Element has a background image.');
651
+
652
+ // Check if the element has accessible labels or text content
653
+ if (
654
+ !hasAccessibleLabel(element) &&
655
+ !hasChildWithAccessibleText(element) &&
656
+ !isAccessibleText(getTextContent(element))
657
+ ) {
658
+ customConsoleWarn(
659
+ 'Flagging element with background image but without accessible label or text.',
660
+ );
661
+ return true; // Flag the element
662
+ } else {
663
+ customConsoleWarn(
664
+ 'Element with background image has accessible label or text, skipping flagging.',
665
+ );
666
+ return false; // Do not flag
667
+ }
668
+ }
669
+
670
+ // **Proceed with ancestor traversal if no background image is found**
671
+ // Traverse ancestors to check for interactive elements with accessible labels
672
+ let ancestor = element.parentElement;
673
+ let depth = 0;
674
+ const maxDepth = 4; // Limit the depth to prevent skipping elements incorrectly
675
+ while (ancestor && depth < maxDepth) {
676
+ // Determine if ancestor is interactive
677
+ const isAncestorInteractive =
678
+ hasPointerCursor(ancestor) ||
679
+ ancestor.hasAttribute('onclick') ||
680
+ ancestor.hasAttribute('role') ||
681
+ (ancestor.hasAttribute('tabindex') && ancestor.getAttribute('tabindex') !== '-1') ||
682
+ ancestor.hasAttribute('jsaction') ||
683
+ ancestor.hasAttribute('jscontroller');
684
+
685
+ if (isAncestorInteractive) {
686
+ // Check if ancestor has accessible label or text content
687
+ if (
688
+ hasAccessibleLabel(ancestor) ||
689
+ isAccessibleText(getTextContent(ancestor)) ||
690
+ hasChildWithAccessibleText(ancestor)
691
+ ) {
692
+ customConsoleWarn(
693
+ 'Ancestor interactive element has accessible label or text content, skipping flagging.',
694
+ );
695
+ return false;
696
+ } else {
697
+ // Ancestor is interactive but lacks accessible labeling
698
+ customConsoleWarn(
699
+ 'Ancestor interactive element lacks accessible label, continue flagging.',
700
+ );
701
+ // Do not skip flagging
702
+ }
703
+ }
704
+ ancestor = ancestor.parentElement;
705
+ depth++;
251
706
  }
252
707
 
253
- // Check if the element itself has an accessible label, text content, or CSS content
254
708
  if (
255
709
  hasAccessibleLabel(element) ||
256
- isAccessibleText(element.textContent) ||
257
- hasCSSContent(element)
710
+ isAccessibleText(getTextContent(element)) ||
711
+ hasChildWithAccessibleText(element)
258
712
  ) {
259
- return true;
713
+ customConsoleWarn(
714
+ 'Not Flagging clickable div or span with pointer cursor with accessible text.',
715
+ );
716
+ return false;
260
717
  }
261
718
 
262
- // If the element has children, ensure at least one of them is accessible
263
- if (element.children.length > 0) {
264
- return Array.from(element.children).some(child => {
265
- // If child is aria-hidden, skip it in the accessibility check
266
- if (child.getAttribute('aria-hidden') === 'true') {
267
- return true;
268
- }
269
- // Recursively check if the child or any of its descendants are accessible
270
- return hasAllChildrenAccessible(child);
271
- });
719
+ // If no interactive ancestor with accessible label is found, flag the element
720
+ customConsoleWarn(
721
+ 'Flagging clickable div or span with pointer cursor and no accessible text.',
722
+ );
723
+ return true;
724
+ }
725
+
726
+ // Skip elements with role="menuitem" and ensure accessibility label for any nested elements
727
+ if (element.getAttribute('role') === 'menuitem') {
728
+ if (hasChildWithAccessibleText(element)) {
729
+ customConsoleWarn('Menuitem element has child with accessible text, skipping flagging.');
730
+ return false;
272
731
  }
732
+ }
273
733
 
274
- // If the element and all its children have no accessible labels or text, it's not accessible
734
+ // Check if the parent element has an accessible label
735
+ const parent = element.closest('[aria-label], [role="button"], [role="link"], a, button');
736
+
737
+ if (parent && (hasAccessibleLabel(parent) || hasChildWithAccessibleText(parent))) {
738
+ customConsoleWarn(
739
+ 'Parent element has an accessible label or accessible child, skipping flagging.',
740
+ );
275
741
  return false;
276
742
  }
277
743
 
278
- function hasChildNotANewInteractWithAccessibleText(element: Element) {
279
- // Helper function to check if the element is a link or button
280
- const isLinkOrButton = (child: Node) => {
281
- if (child instanceof Element) { // Check if the child is an Element
282
- return child.nodeName.toLowerCase() === "a" ||
283
- child.nodeName.toLowerCase() === "button" ||
284
- child.getAttribute('role') === 'link' ||
285
- child.getAttribute('role') === 'button';
286
- }
287
- return false;
288
- };
289
-
290
- // Check element children
291
- const hasAccessibleChildElement = Array.from(element.children).some(child => {
292
- if (child instanceof Element) { // Ensure child is an Element
293
- if (!hasPointerCursor(child)) {
294
- return false;
295
- }
296
-
297
- if (child.nodeName.toLowerCase() === "style" || child.nodeName.toLowerCase() === "script") {
298
- return false;
299
- }
300
-
301
- // Skip children that are aria-hidden or links/buttons
302
- if (child.getAttribute('aria-hidden') === 'true' || isLinkOrButton(child) || !isVisibleFocusAble(child)) {
303
- return false;
304
- }
305
-
306
- // Check if the child element has accessible text or label
307
- return isAccessibleText(getTextContent(child)) || hasAccessibleLabel(child) || hasCSSContent(child);
308
- }
309
- return false;
310
- });
311
-
312
- // Check direct text nodes inside the element itself (like <a>"text"</a>)
313
- const hasDirectAccessibleText = Array.from(element.childNodes).some(node => {
314
- if (node.nodeType === Node.TEXT_NODE) {
315
- const textContent = getTextContent(node);
316
-
317
- // Check if the text contains non-ASCII characters (Unicode)
318
- const containsUnicode = /[^\x00-\x7F]/.test(textContent);
319
-
320
- // If contains non-ASCII characters, validate with isValidUnicode
321
- if (containsUnicode) {
322
- return isValidUnicode(textContent);
323
- }
744
+ // Skip flagging if any child has an accessible label (e.g., <img alt="...">
745
+ if (hasAllChildrenAccessible(element)) {
746
+ customConsoleWarn('Element has child nodes with accessible text.');
747
+ return false;
748
+ }
324
749
 
325
- // Otherwise, just check if it's non-empty text
326
- return textContent.length > 0;
327
- }
750
+ // Check if the <a> element has all children accessible
751
+ if (element.nodeName.toLowerCase() === 'a' && hasAllChildrenAccessible(element)) {
752
+ customConsoleWarn('Hyperlink has all children with accessible labels, skipping flagging.');
753
+ return false;
754
+ }
328
755
 
329
- // Recursively check for text content inside child nodes of elements that are not links or buttons
330
- if (node.nodeType === Node.ELEMENT_NODE && node instanceof Element && !isLinkOrButton(node)) {
331
- return Array.from(node.childNodes).some(innerNode => {
332
- if (innerNode.nodeType === Node.TEXT_NODE) {
333
- const innerTextContent = getTextContent(innerNode).trim();
334
- return innerTextContent && !isValidUnicode(innerTextContent); // Check for non-Unicode content
335
- }
336
- return false;
337
- });
338
- }
756
+ if (element.hasAttribute('tabindex') && element.getAttribute('tabindex') === '-1') {
757
+ customConsoleWarn("Element has tabindex='-1'.");
758
+ return false;
759
+ }
339
760
 
340
- return false;
341
- });
342
-
343
-
344
- return hasAccessibleChildElement || hasDirectAccessibleText;
761
+ const childWithTabindexNegativeOne = Array.from(element.children).some(
762
+ child => child.hasAttribute('tabindex') && child.getAttribute('tabindex') === '-1',
763
+ );
764
+ if (childWithTabindexNegativeOne) {
765
+ customConsoleWarn("Element has a child with tabindex='-1'.");
766
+ return false;
345
767
  }
346
-
347
768
 
348
- const style = document.createElement('style');
349
- style.innerHTML = `
350
- .highlight-flagged {
351
- outline: 4px solid rgba(128, 0, 128, 1) !important; /* Thicker primary outline with purple in rgba format */
352
- box-shadow:
353
- 0 0 25px 15px rgba(255, 255, 255, 1), /* White glow for contrast */
354
- 0 0 15px 10px rgba(144, 33, 166, 1) !important; /* Consistent purple glow in rgba format */
769
+ if (landmarkElements.includes(element.nodeName.toLowerCase())) {
770
+ customConsoleWarn('Element is a landmark element.');
771
+ return false;
355
772
  }
356
- `;
357
- document.head.appendChild(style);
358
773
 
359
- function injectStylesIntoFrame(frame: HTMLIFrameElement) {
360
- try {
361
- const frameDocument = frame.contentDocument || frame.contentWindow.document;
362
- if (frameDocument) {
363
- const frameStyle = frameDocument.createElement('style');
364
- frameStyle.innerHTML = `
365
- .highlight-flagged {
366
- outline: 4px solid rgba(128, 0, 128, 1) !important; /* Thicker primary outline with purple in rgba format */
367
- box-shadow:
368
- 0 0 25px 15px rgba(255, 255, 255, 1), /* White glow for contrast */
369
- 0 0 15px 10px rgba(144, 33, 166, 1) !important; /* Consistent purple glow in rgba format */
370
- }
371
- `;
372
- frameDocument.head.appendChild(frameStyle);
373
- }
374
- } catch (error) {
375
- customConsoleWarn(`Cannot access frame document: ${error}`);
376
- }
774
+ // Prevent flagging <svg> or <icon> if a sibling or parent has an accessible label or if it is part of a button-like element
775
+ if (
776
+ (element.nodeName.toLowerCase() === 'svg' || element.nodeName.toLowerCase() === 'icon') &&
777
+ (element.getAttribute('focusable') === 'false' ||
778
+ hasSiblingOrParentAccessibleLabel(element) ||
779
+ element.closest('[role="button"]') ||
780
+ element.closest('button'))
781
+ ) {
782
+ customConsoleWarn(
783
+ 'Sibling or parent element has an accessible label or svg is part of a button, skipping flagging of svg or icon.',
784
+ );
785
+ return false;
377
786
  }
378
787
 
379
- function hasCSSContent(element: Element) {
380
- const beforeContent = window
381
- .getComputedStyle(element, '::before')
382
- .getPropertyValue('content');
383
- const afterContent = window.getComputedStyle(element, '::after').getPropertyValue('content');
788
+ if (element.nodeName.toLowerCase() === 'svg') {
789
+ const parentGroup = element.closest('g');
790
+ if (parentGroup && parentGroup.querySelector('title')) {
791
+ customConsoleWarn('Parent group element has a <title>, skipping flagging of svg.');
792
+ return false;
793
+ }
794
+ }
384
795
 
385
- function isAccessibleContent(value: string) {
386
- if (!value || value === 'none' || value === 'normal') {
387
- return false;
388
- }
389
- // Remove quotes from the content value
390
- const unquotedValue = value.replace(/^['"]|['"]$/g, '').trim();
796
+ if (element.nodeName.toLowerCase() === 'button') {
797
+ const hasAccessibleLabelForButton =
798
+ hasAccessibleLabel(element) || isAccessibleText(getTextContent(element));
799
+ if (hasAccessibleLabelForButton) {
800
+ customConsoleWarn('Button has an accessible label, skipping flagging.');
801
+ return false;
802
+ }
391
803
 
392
- // Use the isAccessibleText function
393
- return isAccessibleText(unquotedValue);
804
+ const hasSvgChildWithoutLabel = Array.from(element.children).some(
805
+ child => child.nodeName.toLowerCase() === 'svg' && !hasAccessibleLabel(child),
806
+ );
807
+ if (hasSvgChildWithoutLabel) {
808
+ customConsoleWarn('Flagging button with child SVG lacking accessible label.');
809
+ return true;
394
810
  }
811
+ }
395
812
 
396
- return isAccessibleContent(beforeContent) || isAccessibleContent(afterContent);
813
+ if (
814
+ element.nodeName.toLowerCase() === 'input' &&
815
+ (element as HTMLInputElement).type === 'image' &&
816
+ !hasAccessibleLabel(element)
817
+ ) {
818
+ customConsoleWarn("Flagging <input type='image'> without accessible label.");
819
+ return true;
397
820
  }
398
821
 
399
- function isElementTooSmall(element: Element) {
400
- // Get the bounding rectangle of the element
401
- const rect = element.getBoundingClientRect();
822
+ if (element.nodeName.toLowerCase() === 'a') {
823
+ const img = element.querySelector('img');
402
824
 
403
- // Check if width or height is less than 1
404
- return rect.width < 1 || rect.height < 1;
405
- }
825
+ // Log to verify visibility and pointer checks
826
+ customConsoleWarn('Processing <a> element.');
827
+
828
+ // Ensure this <a> does not have an accessible label
829
+ const linkHasAccessibleLabel = hasAccessibleLabel(element);
830
+
831
+ // Ensure the <img> inside <a> does not have an accessible label
832
+ const imgHasAccessibleLabel = img ? hasAccessibleLabel(img) : false;
406
833
 
407
- function getTextContent(element: Element | ChildNode): string {
408
- if (element.nodeType === Node.TEXT_NODE) {
409
- return element.nodeValue?.trim() ?? ''; // Return the text directly if it's a TEXT_NODE
834
+ // Log to verify if <img> has accessible label
835
+ if (img) {
836
+ customConsoleWarn('Found <img> inside <a>. Accessible label: ' + imgHasAccessibleLabel);
837
+ } else {
838
+ customConsoleWarn('No <img> found inside <a>.');
410
839
  }
411
-
412
- let textContent = '';
413
-
414
- for (const node of element.childNodes) {
415
- if (node.nodeType === Node.TEXT_NODE) {
416
- textContent += node.nodeValue?.trim() ?? ''; // Append text content from text nodes
417
- } else if (node.nodeType === Node.ELEMENT_NODE) {
418
- // Type assertion: node is an Element
419
- const elementNode = node as Element;
420
-
421
- // If it's an SVG and has a <title> tag inside it, we want to grab that text
422
- if (elementNode.tagName.toLowerCase() === 'svg') {
423
- const titleElement = elementNode.querySelector('title');
424
- if (titleElement && isVisibleFocusAble(elementNode)) {
425
- return titleElement.textContent?.trim() ?? ''; // Return the title text if valid
426
- }
427
- }
428
-
429
- // Recursively check child elements if it's an element node
430
- if (isVisibleFocusAble(elementNode)) {
431
- const childText = getTextContent(elementNode);
432
- if (childText) {
433
- textContent += childText; // Append valid child text
434
- }
435
- }
436
- }
840
+
841
+ // Skip flagging if <a> has an accessible label or all children are accessible
842
+ if (linkHasAccessibleLabel || hasChildNotANewInteractWithAccessibleText(element)) {
843
+ customConsoleWarn('Hyperlink has an accessible label, skipping flagging.');
844
+ return false;
845
+ }
846
+
847
+ // Flag if both <a> and <img> inside lack accessible labels
848
+ if (!linkHasAccessibleLabel && img && !imgHasAccessibleLabel) {
849
+ customConsoleWarn('Flagging <a> with inaccessible <img>.');
850
+ return true;
851
+ }
852
+
853
+ if (!linkHasAccessibleLabel) {
854
+ customConsoleWarn('Flagging <a> with no accessible label');
855
+ return true;
437
856
  }
438
-
439
- return textContent.trim(); // Return the combined text content
440
857
  }
441
-
442
858
 
443
- function shouldFlagElement(element: HTMLElement, allowNonClickableFlagging: boolean) {
444
- if (isElementTooSmall(element))
445
- {
446
- return false;
447
- }
448
-
449
- // Skip non-clickable elements if allowNonClickableFlagging is false
450
- if (allowNonClickableFlagging && !hasPointerCursor(element)) {
451
- customConsoleWarn("Element is not clickable and allowNonClickableFlagging is false, skipping flagging.");
452
- return false;
453
- }
454
-
455
- // Do not flag elements if any ancestor has aria-hidden="true"
456
- if (element.closest('[aria-hidden="true"]')) {
457
- customConsoleWarn("An ancestor element has aria-hidden='true', skipping flagging.");
458
- return false;
459
- }
460
-
461
- let parents = element.parentElement;
462
-
463
- // Causing false negative of svg
464
- if (parents) {
465
- // Check if the parent has an accessible label
466
- if (hasAccessibleLabel(parents) || hasChildNotANewInteractWithAccessibleText(parents)) {
467
- customConsoleWarn("Parent element has an accessible label, skipping flagging of this element.");
468
- return false;
469
- }
470
-
471
- }
472
-
473
- let maxLayers = 3;
474
- let tracedBackedLayerCount = 0;
475
- while (parents && tracedBackedLayerCount <= maxLayers) {
476
- // Skip flagging if the parent or the element itself has an accessible label
477
- if (hasAccessibleLabel(parents) || hasChildNotANewInteractWithAccessibleText(parents)) {
478
- customConsoleWarn("Parent or element has an accessible label, skipping flagging.",parents);
479
- return false;
480
- }
481
-
482
- // Skip flagging if the parent is a button-like element with aria-expanded
483
- if (
484
- parents.getAttribute('role') === 'button' &&
485
- (parents.hasAttribute('aria-expanded') || parents.hasAttribute('aria-controls'))
486
- ) {
487
- customConsoleWarn("Parent element is an interactive button with aria-expanded or aria-controls, skipping flagging.");
488
- return false;
489
- }
490
-
491
- // Skip flagging if an ancestor has an accessible label or an interactive role (e.g., button, link)
492
- if (
493
- ['div', 'section', 'article', 'nav'].includes(parents.nodeName.toLowerCase()) &&
494
- hasAccessibleLabel(parents)
495
- ) {
496
- customConsoleWarn("Ancestor element with contextual role has an accessible label, skipping flagging.");
497
- return false;
498
- }
499
-
500
- // Skip flag if parent is an a link or button that already contains accessible text
501
- if (
502
- (parents.nodeName.toLowerCase() === "a" || parents.nodeName.toLowerCase() === "button" ||
503
- parents.getAttribute('role') === 'link' || parents.getAttribute('role') === 'button') && hasChildWithAccessibleText(parents)
504
- ){
505
- return false;
506
- }
507
-
508
- parents = parents.parentElement;
509
- tracedBackedLayerCount++;
510
- }
511
-
512
-
513
- // Skip elements with role="menuitem" if an accessible sibling, parent, or child is present
514
- if (element.getAttribute('role') === 'menuitem') {
515
- if (hasSiblingWithAccessibleLabel(element) || hasChildWithAccessibleText(element) || hasAccessibleLabel(element.parentElement)) {
516
- customConsoleWarn("Menuitem element or its sibling/parent has an accessible label, skipping flagging.");
517
- return false;
518
- }
519
- }
520
-
521
- // Skip flagging child elements if the parent element has role="menuitem" and is accessible
522
- const parentMenuItem = element.closest('[role="menuitem"]');
523
- if (parentMenuItem && (hasAccessibleLabel(parentMenuItem) || hasChildWithAccessibleText(parentMenuItem))) {
524
- customConsoleWarn("Parent menuitem element has an accessible label or child with accessible text, skipping flagging of its children.");
525
- return false;
526
- }
527
-
528
- // Add the new condition for empty div or span elements without any accessible text or children with accessible labels
529
- if ((element.nodeName.toLowerCase() === 'span' || element.nodeName.toLowerCase() === 'div') &&
530
- element.children.length === 0 && getTextContent(element).trim().length === 0) {
531
- const parent = element.parentElement;
532
- if (parent) {
533
- const hasAccessibleChild = Array.from(parent.children).some(child =>
534
- child !== element && hasAccessibleLabel(child)
535
- );
536
-
537
- if (hasAccessibleChild) {
538
- customConsoleWarn("Parent element has an accessible child, skipping flagging of empty span or div.");
539
- return false;
540
- }
541
- }
542
- }
543
-
544
- // Do not flag elements with aria-hidden="true"
545
- if (element.getAttribute('aria-hidden') === 'true') {
546
- customConsoleWarn("Element is aria-hidden, skipping flagging.");
547
- return false;
548
- }
549
-
550
- if (element.getAttribute("aria-labelledby") !== null && element.getAttribute("aria-labelledby") !== "") {
551
- // Get the list of IDs referenced in aria-labelledby
552
- const ids = element.getAttribute("aria-labelledby").split(' ');
553
- let shouldNotFlag = false
554
-
555
- // Loop through each ID and find the corresponding elements
556
- ids.forEach(id => {
557
- const referencedElement = document.getElementById(id);
558
-
559
- // Check if the element was found
560
- if (referencedElement &&
561
- (hasAccessibleLabel(referencedElement) ||
562
- isAccessibleText(getTextContent(referencedElement)) ||
563
- hasAllChildrenAccessible(referencedElement) ))
564
- {
565
- shouldNotFlag = true;
566
- }
567
- });
568
-
569
- if (shouldNotFlag)
570
- {
571
- return false
572
- }
573
-
574
- }
575
-
576
- // Do not flag elements with role="presentation"
577
- if (element.getAttribute('role') === 'presentation') {
578
- customConsoleWarn("Element has role='presentation', skipping flagging.");
579
- return false;
580
- }
581
-
582
- if (element.dataset.flagged === 'true') {
583
- customConsoleWarn("Element is already flagged.");
584
- return false;
585
- }
586
-
587
- // If an ancestor element is flagged, do not flag this element
588
- if (element.closest('[data-flagged="true"]')) {
589
- customConsoleWarn("An ancestor element is already flagged.");
590
- return false;
591
- }
592
-
593
- // Skip elements that are not visible (e.g., display:none)
594
- const computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
595
- if (computedStyle.display === 'none' || computedStyle.visibility === 'hidden' || element.offsetParent === null) {
596
- customConsoleWarn("Element is not visible, skipping flagging.");
597
- return false;
859
+ // Modify this section for generic elements
860
+ if (['span', 'div', 'icon', 'svg', 'button'].includes(element.nodeName.toLowerCase())) {
861
+ if (element.nodeName.toLowerCase() === 'icon' || element.nodeName.toLowerCase() === 'svg') {
862
+ // Check if the element has an accessible label or if it has a sibling, parent, or summary/related element that provides an accessible label
863
+ if (
864
+ !hasAccessibleLabel(element) &&
865
+ !hasSiblingOrParentAccessibleLabel(element) &&
866
+ !hasSummaryOrDetailsLabel(element) &&
867
+ element.getAttribute('focusable') !== 'false'
868
+ ) {
869
+ customConsoleWarn('Flagging icon or svg without accessible label.');
870
+ return true;
598
871
  }
599
-
600
- // Skip empty <div> or <span> elements without any accessible text or children with accessible labels, unless they have a pointer cursor
601
- if ((element.nodeName.toLowerCase() === 'div' || element.nodeName.toLowerCase() === 'span') &&
602
- element.children.length === 0 && getTextContent(element).trim().length === 0) {
603
-
604
- if (!hasPointerCursor(element)) {
605
- customConsoleWarn("Empty div or span without accessible text and without pointer cursor, skipping flagging.");
606
- return false;
607
- }
608
-
609
- // **New background-image check**
610
- const backgroundImage = window.getComputedStyle(element).getPropertyValue('background-image');
611
- if (backgroundImage && backgroundImage !== 'none') {
612
- customConsoleWarn("Element has a background image.");
613
-
614
- // Check if the element has accessible labels or text content
615
- if (!hasAccessibleLabel(element) && !hasChildWithAccessibleText(element) && !isAccessibleText(getTextContent(element))) {
616
- customConsoleWarn("Flagging element with background image but without accessible label or text.");
617
- return true; // Flag the element
618
- } else {
619
- customConsoleWarn("Element with background image has accessible label or text, skipping flagging.");
620
- return false; // Do not flag
621
- }
622
- }
623
-
624
- // **Proceed with ancestor traversal if no background image is found**
625
- // Traverse ancestors to check for interactive elements with accessible labels
626
- let ancestor = element.parentElement;
627
- let depth = 0;
628
- const maxDepth = 4; // Limit the depth to prevent skipping elements incorrectly
629
- while (ancestor && depth < maxDepth) {
630
- // Determine if ancestor is interactive
631
- const isAncestorInteractive = hasPointerCursor(ancestor) ||
632
- ancestor.hasAttribute('onclick') ||
633
- ancestor.hasAttribute('role') ||
634
- (ancestor.hasAttribute('tabindex') && ancestor.getAttribute('tabindex') !== '-1') ||
635
- ancestor.hasAttribute('jsaction') ||
636
- ancestor.hasAttribute('jscontroller');
637
-
638
- if (isAncestorInteractive) {
639
- // Check if ancestor has accessible label or text content
640
- if (hasAccessibleLabel(ancestor) || isAccessibleText(getTextContent(ancestor)) || hasChildWithAccessibleText(ancestor)) {
641
- customConsoleWarn("Ancestor interactive element has accessible label or text content, skipping flagging.");
642
- return false;
643
- } else {
644
- // Ancestor is interactive but lacks accessible labeling
645
- customConsoleWarn("Ancestor interactive element lacks accessible label, continue flagging.");
646
- // Do not skip flagging
647
- }
648
- }
649
- ancestor = ancestor.parentElement;
650
- depth++;
651
- }
652
-
653
- if (hasAccessibleLabel(element) || isAccessibleText(getTextContent(element)) || hasChildWithAccessibleText(element))
654
- {
655
- customConsoleWarn("Not Flagging clickable div or span with pointer cursor with accessible text.");
656
- return false;
657
- }
872
+ return false;
873
+ }
658
874
 
659
- // If no interactive ancestor with accessible label is found, flag the element
660
- customConsoleWarn("Flagging clickable div or span with pointer cursor and no accessible text.");
661
- return true;
662
- }
663
-
664
- // Skip elements with role="menuitem" and ensure accessibility label for any nested elements
665
- if (element.getAttribute('role') === 'menuitem') {
666
- if (hasChildWithAccessibleText(element)) {
667
- customConsoleWarn("Menuitem element has child with accessible text, skipping flagging.");
668
- return false;
669
- }
670
- }
671
-
672
- // Check if the parent element has an accessible label
673
- const parent = element.closest('[aria-label], [role="button"], [role="link"], a, button');
674
-
675
- if (parent && (hasAccessibleLabel(parent) || hasChildWithAccessibleText(parent))) {
676
- customConsoleWarn("Parent element has an accessible label or accessible child, skipping flagging.");
677
- return false;
678
- }
679
-
680
- // Skip flagging if any child has an accessible label (e.g., <img alt="...">
681
- if (hasAllChildrenAccessible(element)) {
682
- customConsoleWarn("Element has child nodes with accessible text.");
683
- return false;
684
- }
685
-
686
- // Check if the <a> element has all children accessible
687
- if (element.nodeName.toLowerCase() === 'a' && hasAllChildrenAccessible(element)) {
688
- customConsoleWarn("Hyperlink has all children with accessible labels, skipping flagging.");
689
- return false;
690
- }
691
-
692
- if (element.hasAttribute('tabindex') && element.getAttribute('tabindex') === '-1') {
693
- customConsoleWarn("Element has tabindex='-1'.");
694
- return false;
695
- }
696
-
697
- const childWithTabindexNegativeOne = Array.from(element.children).some(child =>
698
- child.hasAttribute('tabindex') && child.getAttribute('tabindex') === '-1'
699
- );
700
- if (childWithTabindexNegativeOne) {
701
- customConsoleWarn("Element has a child with tabindex='-1'.");
702
- return false;
703
- }
704
-
705
- if (landmarkElements.includes(element.nodeName.toLowerCase())) {
706
- customConsoleWarn("Element is a landmark element.");
707
- return false;
708
- }
709
-
710
- // Prevent flagging <svg> or <icon> if a sibling or parent has an accessible label or if it is part of a button-like element
711
- if ((element.nodeName.toLowerCase() === 'svg' || element.nodeName.toLowerCase() === 'icon') && (element.getAttribute('focusable') === 'false' || hasSiblingOrParentAccessibleLabel(element) || element.closest('[role="button"]') || element.closest('button'))) {
712
- customConsoleWarn("Sibling or parent element has an accessible label or svg is part of a button, skipping flagging of svg or icon.");
713
- return false;
714
- }
715
-
716
- if (element.nodeName.toLowerCase() === 'svg') {
717
- const parentGroup = element.closest('g');
718
- if (parentGroup && parentGroup.querySelector('title')) {
719
- customConsoleWarn("Parent group element has a <title>, skipping flagging of svg.");
720
- return false;
721
- }
722
- }
723
-
724
- if (element.nodeName.toLowerCase() === 'button') {
725
- const hasAccessibleLabelForButton = hasAccessibleLabel(element) || isAccessibleText(getTextContent(element));
726
- if (hasAccessibleLabelForButton) {
727
- customConsoleWarn("Button has an accessible label, skipping flagging.");
728
- return false;
729
- }
730
-
731
- const hasSvgChildWithoutLabel = Array.from(element.children).some(child => child.nodeName.toLowerCase() === 'svg' && !hasAccessibleLabel(child));
732
- if (hasSvgChildWithoutLabel) {
733
- customConsoleWarn("Flagging button with child SVG lacking accessible label.");
734
- return true;
735
- }
736
- }
737
-
738
- if (element.nodeName.toLowerCase() === 'input' && (element as HTMLInputElement).type === 'image' && !hasAccessibleLabel(element)) {
739
- customConsoleWarn("Flagging <input type='image'> without accessible label.");
740
- return true;
875
+ if (getTextContent(element).trim().length > 0) {
876
+ customConsoleWarn('Element has valid text content.');
877
+ return false;
741
878
  }
742
-
743
- if (element.nodeName.toLowerCase() === 'a') {
744
- const img = element.querySelector('img');
745
-
746
- // Log to verify visibility and pointer checks
747
- customConsoleWarn("Processing <a> element.");
748
-
749
- // Ensure this <a> does not have an accessible label
750
- const linkHasAccessibleLabel = hasAccessibleLabel(element);
751
-
752
- // Ensure the <img> inside <a> does not have an accessible label
753
- const imgHasAccessibleLabel = img ? hasAccessibleLabel(img) : false;
754
-
755
- // Log to verify if <img> has accessible label
756
- if (img) {
757
- customConsoleWarn("Found <img> inside <a>. Accessible label: " + imgHasAccessibleLabel);
758
- } else {
759
- customConsoleWarn("No <img> found inside <a>.");
760
- }
761
-
762
-
763
- // Skip flagging if <a> has an accessible label or all children are accessible
764
- if (linkHasAccessibleLabel || hasChildNotANewInteractWithAccessibleText(element)) {
765
- customConsoleWarn("Hyperlink has an accessible label, skipping flagging.");
766
- return false;
767
- }
768
-
769
- // Flag if both <a> and <img> inside lack accessible labels
770
- if (!linkHasAccessibleLabel && img && !imgHasAccessibleLabel) {
771
- customConsoleWarn("Flagging <a> with inaccessible <img>.");
772
- return true;
773
- }
774
-
775
- if (!linkHasAccessibleLabel)
776
- {
777
- customConsoleWarn("Flagging <a> with no accessible label");
778
- return true;
779
- }
780
- }
781
-
782
- // Modify this section for generic elements
783
- if (['span', 'div', 'icon', 'svg', 'button'].includes(element.nodeName.toLowerCase())) {
784
- if (element.nodeName.toLowerCase() === 'icon' || element.nodeName.toLowerCase() === 'svg') {
785
- // Check if the element has an accessible label or if it has a sibling, parent, or summary/related element that provides an accessible label
786
- if (!hasAccessibleLabel(element) && !hasSiblingOrParentAccessibleLabel(element) && !hasSummaryOrDetailsLabel(element) && element.getAttribute('focusable') !== 'false') {
787
- customConsoleWarn("Flagging icon or svg without accessible label.");
788
- return true;
789
- }
790
- return false;
791
- }
792
-
793
- if (getTextContent(element).trim().length > 0) {
794
- customConsoleWarn("Element has valid text content.");
795
- return false;
796
- }
797
-
798
- if (element.hasAttribute('aria-label') && element.getAttribute('aria-label').trim().length > 0) {
799
- customConsoleWarn("Element has an aria-label attribute, skipping flagging.");
800
- return false;
801
- }
802
- }
803
-
804
- if (element.nodeName.toLowerCase() === 'div') {
805
- const flaggedChild = Array.from(element.children).some(child => (child as HTMLElement).dataset.flagged === 'true');
806
- if (flaggedChild) {
807
- customConsoleWarn("Div contains a flagged child, flagging only outermost element.");
808
- return false;
809
- }
810
-
811
- // Update this condition to include hasChildWithAccessibleText
812
- if (getTextContent(element).trim().length > 0 || hasChildWithAccessibleText(element)) {
813
- customConsoleWarn("Div has valid text content or child with accessible text.");
814
- return false;
815
- }
816
-
817
- const img = element.querySelector('img');
818
- if (img) {
819
- const altText = img.getAttribute('alt');
820
- const ariaLabel = img.getAttribute('aria-label');
821
- const ariaLabelledByText = getAriaLabelledByText(img);
822
- if (altText !== null || ariaLabel || ariaLabelledByText) {
823
- customConsoleWarn("Div contains an accessible img or an img with an alt attribute (even if empty).");
824
- return false;
825
- }
826
- }
827
-
828
- const svg = element.querySelector('svg');
829
- if (svg) {
830
- if (hasPointerCursor(element) && !hasAccessibleLabel(svg) && !hasSummaryOrDetailsLabel(svg) && svg.getAttribute('focusable') !== 'false') {
831
- customConsoleWarn("Flagging clickable div with SVG without accessible label.");
832
- return true;
833
- }
834
- }
835
-
836
- if (hasPointerCursor(element) && !hasAccessibleLabel(element) && !isAccessibleText(getTextContent(element))) {
837
- customConsoleWarn("Clickable div without accessible label or text content.");
838
- return true;
839
- }
840
- }
841
-
842
- if (element.nodeName.toLowerCase() === 'img' || element.nodeName.toLowerCase() === 'picture') {
843
- const imgElement = element.nodeName.toLowerCase() === 'picture' ? element.querySelector('img') : element;
844
- const altText = imgElement.getAttribute('alt');
845
- const ariaLabel = imgElement.getAttribute('aria-label');
846
- const ariaLabelledByText = getAriaLabelledByText(imgElement);
847
-
848
- if (!allowNonClickableFlagging) {
849
- if (!imgElement.closest('a') && !imgElement.closest('button') && !hasPointerCursor(imgElement) && !(altText !== null) && !(ariaLabel && ariaLabel.trim().length > 0) && !(ariaLabelledByText && ariaLabelledByText.length > 0)) {
850
- customConsoleWarn("Non-clickable image ignored.");
851
- return false;
852
- }
853
- }
854
-
855
- if (!imgElement.closest('a') && !imgElement.closest('button') && !(altText !== null) && !(ariaLabel && ariaLabel.trim().length > 0) && !(ariaLabelledByText && ariaLabelledByText.length > 0)) {
856
- customConsoleWarn("Flagging img or picture without accessible label.");
857
- return true;
858
- }
879
+
880
+ if (
881
+ element.hasAttribute('aria-label') &&
882
+ element.getAttribute('aria-label').trim().length > 0
883
+ ) {
884
+ customConsoleWarn('Element has an aria-label attribute, skipping flagging.');
885
+ return false;
886
+ }
887
+ }
888
+
889
+ if (element.nodeName.toLowerCase() === 'div') {
890
+ const flaggedChild = Array.from(element.children).some(
891
+ child => (child as HTMLElement).dataset.flagged === 'true',
892
+ );
893
+ if (flaggedChild) {
894
+ customConsoleWarn('Div contains a flagged child, flagging only outermost element.');
895
+ return false;
896
+ }
897
+
898
+ // Update this condition to include hasChildWithAccessibleText
899
+ if (getTextContent(element).trim().length > 0 || hasChildWithAccessibleText(element)) {
900
+ customConsoleWarn('Div has valid text content or child with accessible text.');
901
+ return false;
902
+ }
903
+
904
+ const img = element.querySelector('img');
905
+ if (img) {
906
+ const altText = img.getAttribute('alt');
907
+ const ariaLabel = img.getAttribute('aria-label');
908
+ const ariaLabelledByText = getAriaLabelledByText(img);
909
+ if (altText !== null || ariaLabel || ariaLabelledByText) {
910
+ customConsoleWarn(
911
+ 'Div contains an accessible img or an img with an alt attribute (even if empty).',
912
+ );
913
+ return false;
859
914
  }
860
-
861
- // Additional check to skip divs with empty children or child-child elements
862
- const areAllDescendantsEmpty = Array.from(element.querySelectorAll('*')).every(child => getTextContent(child).trim().length === 0 && !hasAccessibleLabel(child));
863
- if (element.nodeName.toLowerCase() === 'div' && areAllDescendantsEmpty) {
864
- customConsoleWarn("Div with empty descendants, skipping flagging.");
865
- return false;
915
+ }
916
+
917
+ const svg = element.querySelector('svg');
918
+ if (svg) {
919
+ if (
920
+ hasPointerCursor(element) &&
921
+ !hasAccessibleLabel(svg) &&
922
+ !hasSummaryOrDetailsLabel(svg) &&
923
+ svg.getAttribute('focusable') !== 'false'
924
+ ) {
925
+ customConsoleWarn('Flagging clickable div with SVG without accessible label.');
926
+ return true;
866
927
  }
867
-
868
- if (hasCSSContent(element)) {
869
- customConsoleWarn("Element has CSS ::before or ::after content, skipping flagging.");
870
- return false;
928
+ }
929
+
930
+ if (
931
+ hasPointerCursor(element) &&
932
+ !hasAccessibleLabel(element) &&
933
+ !isAccessibleText(getTextContent(element))
934
+ ) {
935
+ customConsoleWarn('Clickable div without accessible label or text content.');
936
+ return true;
937
+ }
938
+ }
939
+
940
+ if (element.nodeName.toLowerCase() === 'img' || element.nodeName.toLowerCase() === 'picture') {
941
+ const imgElement =
942
+ element.nodeName.toLowerCase() === 'picture' ? element.querySelector('img') : element;
943
+ const altText = imgElement.getAttribute('alt');
944
+ const ariaLabel = imgElement.getAttribute('aria-label');
945
+ const ariaLabelledByText = getAriaLabelledByText(imgElement);
946
+
947
+ if (!allowNonClickableFlagging) {
948
+ if (
949
+ !imgElement.closest('a') &&
950
+ !imgElement.closest('button') &&
951
+ !hasPointerCursor(imgElement) &&
952
+ !(altText !== null) &&
953
+ !(ariaLabel && ariaLabel.trim().length > 0) &&
954
+ !(ariaLabelledByText && ariaLabelledByText.length > 0)
955
+ ) {
956
+ customConsoleWarn('Non-clickable image ignored.');
957
+ return false;
871
958
  }
872
-
873
- return false; // Default case: do not flag
959
+ }
960
+
961
+ if (
962
+ !imgElement.closest('a') &&
963
+ !imgElement.closest('button') &&
964
+ !(altText !== null) &&
965
+ !(ariaLabel && ariaLabel.trim().length > 0) &&
966
+ !(ariaLabelledByText && ariaLabelledByText.length > 0)
967
+ ) {
968
+ customConsoleWarn('Flagging img or picture without accessible label.');
969
+ return true;
970
+ }
874
971
  }
875
972
 
876
- function flagElements() {
973
+ // Additional check to skip divs with empty children or child-child elements
974
+ const areAllDescendantsEmpty = Array.from(element.querySelectorAll('*')).every(
975
+ child => getTextContent(child).trim().length === 0 && !hasAccessibleLabel(child),
976
+ );
977
+ if (element.nodeName.toLowerCase() === 'div' && areAllDescendantsEmpty) {
978
+ customConsoleWarn('Div with empty descendants, skipping flagging.');
979
+ return false;
980
+ }
877
981
 
878
- const currentFlaggedElementsByDocument: Record<string, HTMLElement[]> = {}; // Temporary object to hold current flagged elements
982
+ if (hasCSSContent(element)) {
983
+ customConsoleWarn('Element has CSS ::before or ::after content, skipping flagging.');
984
+ return false;
985
+ }
986
+
987
+ return false; // Default case: do not flag
988
+ }
989
+
990
+ function flagElements() {
991
+ const currentFlaggedElementsByDocument: Record<string, HTMLElement[]> = {}; // Temporary object to hold current flagged elements
879
992
 
880
- /*
993
+ /*
881
994
  Collects all the elements and places then into an array
882
995
  Then places the array in the correct frame
883
996
  */
884
- // Process main document
885
- const currentFlaggedElements: HTMLElement[] = [];
886
- const allElements = Array.from(document.querySelectorAll<HTMLElement>('*'));
887
- let indexofAllElements: number = 0;
888
-
889
- while (indexofAllElements < allElements.length) {
890
- const element = allElements[indexofAllElements] as HTMLElement;
891
- // if it selects a frameset
892
- if (
893
- shouldFlagElement(element, allowNonClickableFlagging) ||
894
- element.dataset.flagged === 'true'
895
- ) {
896
- element.dataset.flagged = 'true'; // Mark element as flagged
897
- currentFlaggedElements.push(element);
898
- }
997
+ // Process main document
998
+ const currentFlaggedElements: HTMLElement[] = [];
999
+ const allElements = Array.from(document.querySelectorAll<HTMLElement>('*'));
1000
+ let indexofAllElements: number = 0;
1001
+
1002
+ while (indexofAllElements < allElements.length) {
1003
+ const element = allElements[indexofAllElements] as HTMLElement;
1004
+ // if it selects a frameset
1005
+ if (
1006
+ shouldFlagElement(element, allowNonClickableFlagging) ||
1007
+ element.dataset.flagged === 'true'
1008
+ ) {
1009
+ element.dataset.flagged = 'true'; // Mark element as flagged
1010
+ currentFlaggedElements.push(element);
1011
+ }
899
1012
 
900
- // If the element has a shadowRoot, add its children
901
- if (element.shadowRoot) {
902
- allElements.push(
903
- ...(Array.from(element.shadowRoot.querySelectorAll('*')) as HTMLElement[]),
904
- );
905
- }
906
- indexofAllElements++;
1013
+ // If the element has a shadowRoot, add its children
1014
+ if (element.shadowRoot) {
1015
+ allElements.push(
1016
+ ...(Array.from(element.shadowRoot.querySelectorAll('*')) as HTMLElement[]),
1017
+ );
907
1018
  }
908
- currentFlaggedElementsByDocument[''] = currentFlaggedElements; // Key "" represents the main document
909
-
910
- // Process iframes
911
- const iframes = document.querySelectorAll('iframe');
912
- iframes.forEach((iframe, index) => {
913
- injectStylesIntoFrame(iframe);
914
- try {
915
- const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
916
- if (iframeDocument) {
917
- const iframeFlaggedElements: HTMLElement[] = [];
918
- const iframeElements = Array.from(iframeDocument.querySelectorAll<HTMLElement>('*'));
919
- let indexOfIframeElements: number = 0;
920
- while (indexOfIframeElements < iframeElements.length) {
921
- const element = iframeElements[indexOfIframeElements] as HTMLElement;
922
- if (
923
- shouldFlagElement(element, allowNonClickableFlagging) ||
924
- element.dataset.flagged === 'true'
925
- ) {
926
- element.dataset.flagged = 'true'; // Mark element as flagged
927
- iframeFlaggedElements.push(element);
928
- }
929
- // If the element has a shadowRoot, add its children
930
- if (element.shadowRoot) {
931
- iframeElements.push(
932
- ...(Array.from(element.shadowRoot.querySelectorAll('*')) as HTMLElement[]),
933
- );
934
- }
935
- indexOfIframeElements++;
936
- }
937
- const iframeXPath = getXPath(iframe);
938
- currentFlaggedElementsByDocument[iframeXPath] = iframeFlaggedElements;
939
- }
940
- } catch (error) {
941
- console.warn(`Cannot access iframe document (${index}): ${error.message}`);
942
- }
943
- });
1019
+ indexofAllElements++;
1020
+ }
1021
+ currentFlaggedElementsByDocument[''] = currentFlaggedElements; // Key "" represents the main document
944
1022
 
945
- // Process frames
946
- const frames = document.querySelectorAll('frame');
947
- frames.forEach((frame, index) => {
948
- // injectStylesIntoFrame(frame);
949
- try {
950
- const iframeDocument = frame.contentDocument || frame.contentWindow.document;
951
- if (iframeDocument) {
952
- const iframeFlaggedElements: HTMLElement[] = [];
953
- const iframeElements = Array.from(iframeDocument.querySelectorAll<HTMLElement>('*'));
954
- let indexOfIframeElements: number = 0;
955
- while (indexOfIframeElements < iframeElements.length) {
956
- const element = iframeElements[indexOfIframeElements] as HTMLElement;
957
- if (
958
- shouldFlagElement(element, allowNonClickableFlagging) ||
959
- element.dataset.flagged === 'true'
960
- ) {
961
- element.dataset.flagged = 'true'; // Mark element as flagged
962
- iframeFlaggedElements.push(element);
963
- }
964
- // If the element has a shadowRoot, add its children
965
- if (element.shadowRoot) {
966
- iframeElements.push(
967
- ...(Array.from(element.shadowRoot.querySelectorAll('*')) as HTMLElement[]),
968
- );
969
- }
970
- indexOfIframeElements++;
1023
+ // Process iframes
1024
+ const iframes = document.querySelectorAll('iframe');
1025
+ iframes.forEach((iframe, index) => {
1026
+ injectStylesIntoFrame(iframe);
1027
+ try {
1028
+ const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
1029
+ if (iframeDocument) {
1030
+ const iframeFlaggedElements: HTMLElement[] = [];
1031
+ const iframeElements = Array.from(iframeDocument.querySelectorAll<HTMLElement>('*'));
1032
+ let indexOfIframeElements: number = 0;
1033
+ while (indexOfIframeElements < iframeElements.length) {
1034
+ const element = iframeElements[indexOfIframeElements] as HTMLElement;
1035
+ if (
1036
+ shouldFlagElement(element, allowNonClickableFlagging) ||
1037
+ element.dataset.flagged === 'true'
1038
+ ) {
1039
+ element.dataset.flagged = 'true'; // Mark element as flagged
1040
+ iframeFlaggedElements.push(element);
1041
+ }
1042
+ // If the element has a shadowRoot, add its children
1043
+ if (element.shadowRoot) {
1044
+ iframeElements.push(
1045
+ ...(Array.from(element.shadowRoot.querySelectorAll('*')) as HTMLElement[]),
1046
+ );
971
1047
  }
972
- const iframeXPath = getXPath(frame);
973
- currentFlaggedElementsByDocument[iframeXPath] = iframeFlaggedElements;
1048
+ indexOfIframeElements++;
974
1049
  }
975
- } catch (error) {
976
- console.warn(`Cannot access iframe document (${index}): ${error.message}`);
1050
+ const iframeXPath = getXPath(iframe);
1051
+ currentFlaggedElementsByDocument[iframeXPath] = iframeFlaggedElements;
977
1052
  }
978
- });
1053
+ } catch (error) {
1054
+ console.warn(`Cannot access iframe document (${index}): ${error.message}`);
1055
+ }
1056
+ });
979
1057
 
980
- // Collect XPaths and outerHTMLs of flagged elements per document
981
- const flaggedXPathsByDocument = {};
982
-
983
- for (const docKey in currentFlaggedElementsByDocument) {
984
- const elements = currentFlaggedElementsByDocument[docKey];
985
- const flaggedInfo = []; // Array to hold flagged element info
986
- elements.forEach(flaggedElement => {
987
- const parentFlagged = flaggedElement.closest('[data-flagged="true"]');
988
- if (!parentFlagged || parentFlagged === flaggedElement) {
989
- let xpath = getXPath(flaggedElement);
990
- if (docKey !== '') {
991
- // For elements in iframes, adjust XPath
992
- xpath = docKey + xpath;
1058
+ // Process frames
1059
+ const frames = document.querySelectorAll('frame');
1060
+ frames.forEach((frame, index) => {
1061
+ // injectStylesIntoFrame(frame);
1062
+ try {
1063
+ const iframeDocument = frame.contentDocument || frame.contentWindow.document;
1064
+ if (iframeDocument) {
1065
+ const iframeFlaggedElements: HTMLElement[] = [];
1066
+ const iframeElements = Array.from(iframeDocument.querySelectorAll<HTMLElement>('*'));
1067
+ let indexOfIframeElements: number = 0;
1068
+ while (indexOfIframeElements < iframeElements.length) {
1069
+ const element = iframeElements[indexOfIframeElements] as HTMLElement;
1070
+ if (
1071
+ shouldFlagElement(element, allowNonClickableFlagging) ||
1072
+ element.dataset.flagged === 'true'
1073
+ ) {
1074
+ element.dataset.flagged = 'true'; // Mark element as flagged
1075
+ iframeFlaggedElements.push(element);
993
1076
  }
994
- if (xpath && flaggedElement !== null && flaggedElement.outerHTML) {
995
- const { outerHTML } = flaggedElement; // Get outerHTML
996
- flaggedInfo.push({ xpath, code: outerHTML }); // Store xpath and outerHTML
997
-
998
- // Check if the xpath already exists in previousAllFlaggedElementsXPaths
999
- const alreadyExists = previousAllFlaggedElementsXPaths.some(
1000
- entry => entry.xpath === xpath,
1077
+ // If the element has a shadowRoot, add its children
1078
+ if (element.shadowRoot) {
1079
+ iframeElements.push(
1080
+ ...(Array.from(element.shadowRoot.querySelectorAll('*')) as HTMLElement[]),
1001
1081
  );
1002
- if (!alreadyExists) {
1003
- // Add to previousAllFlaggedElementsXPaths only if not already present
1004
- previousAllFlaggedElementsXPaths.push({ xpath, code: outerHTML });
1005
- }
1006
1082
  }
1083
+ indexOfIframeElements++;
1007
1084
  }
1008
- });
1009
- flaggedXPathsByDocument[docKey] = flaggedInfo; // Store all flagged element info
1085
+ const iframeXPath = getXPath(frame);
1086
+ currentFlaggedElementsByDocument[iframeXPath] = iframeFlaggedElements;
1087
+ }
1088
+ } catch (error) {
1089
+ console.warn(`Cannot access iframe document (${index}): ${error.message}`);
1010
1090
  }
1091
+ });
1092
+
1093
+ // Collect XPaths and outerHTMLs of flagged elements per document
1094
+ const flaggedXPathsByDocument = {};
1095
+
1096
+ for (const docKey in currentFlaggedElementsByDocument) {
1097
+ const elements = currentFlaggedElementsByDocument[docKey];
1098
+ const flaggedInfo = []; // Array to hold flagged element info
1099
+ elements.forEach(flaggedElement => {
1100
+ const parentFlagged = flaggedElement.closest('[data-flagged="true"]');
1101
+ if (!parentFlagged || parentFlagged === flaggedElement) {
1102
+ let xpath = getXPath(flaggedElement);
1103
+ if (docKey !== '') {
1104
+ // For elements in iframes, adjust XPath
1105
+ xpath = docKey + xpath;
1106
+ }
1107
+ if (xpath && flaggedElement !== null && flaggedElement.outerHTML) {
1108
+ const { outerHTML } = flaggedElement; // Get outerHTML
1109
+ flaggedInfo.push({ xpath, code: outerHTML }); // Store xpath and outerHTML
1110
+
1111
+ // Check if the xpath already exists in previousAllFlaggedElementsXPaths
1112
+ const alreadyExists = previousAllFlaggedElementsXPaths.some(
1113
+ entry => entry.xpath === xpath,
1114
+ );
1115
+ if (!alreadyExists) {
1116
+ // Add to previousAllFlaggedElementsXPaths only if not already present
1117
+ previousAllFlaggedElementsXPaths.push({ xpath, code: outerHTML });
1118
+ }
1119
+ }
1120
+ }
1121
+ });
1122
+ flaggedXPathsByDocument[docKey] = flaggedInfo; // Store all flagged element info
1123
+ }
1011
1124
 
1012
- // Update previousFlaggedXPathsByDocument before finishing
1013
- previousFlaggedXPathsByDocument = { ...flaggedXPathsByDocument };
1125
+ // Update previousFlaggedXPathsByDocument before finishing
1126
+ previousFlaggedXPathsByDocument = { ...flaggedXPathsByDocument };
1014
1127
 
1015
- cleanupFlaggedElements();
1016
- return previousAllFlaggedElementsXPaths;
1017
- }
1128
+ cleanupFlaggedElements();
1129
+ return previousAllFlaggedElementsXPaths;
1130
+ }
1018
1131
 
1019
- // Clean up [data-flagged="true"] attribute added by this script
1020
- function cleanupFlaggedElements() {
1021
- const flaggedElements = document.querySelectorAll('[data-flagged="true"]');
1022
- flaggedElements.forEach(flaggedElement => {
1023
- flaggedElement.removeAttribute('data-flagged');
1024
- });
1025
- }
1026
- function debounce(func, wait) {
1027
- let timeout;
1028
- return function (...args) {
1029
- clearTimeout(timeout);
1030
- timeout = setTimeout(() => func.apply(this, args), wait);
1031
- };
1032
- }
1132
+ // Clean up [data-flagged="true"] attribute added by this script
1133
+ function cleanupFlaggedElements() {
1134
+ const flaggedElements = document.querySelectorAll('[data-flagged="true"]');
1135
+ flaggedElements.forEach(flaggedElement => {
1136
+ flaggedElement.removeAttribute('data-flagged');
1137
+ });
1138
+ }
1033
1139
 
1034
- return flagElements();
1035
- });
1140
+ return flagElements();
1036
1141
  };