@govtechsg/oobee 0.10.36 → 0.10.42

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