@govtechsg/oobee 0.10.76 → 0.10.78-alpha1

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