@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.
- package/.github/workflows/docker-test.yml +1 -1
- package/DETAILS.md +3 -3
- package/INTEGRATION.md +142 -53
- package/README.md +17 -0
- package/REPORTS.md +362 -0
- package/exclusions.txt +4 -1
- package/package.json +2 -2
- package/src/constants/cliFunctions.ts +0 -7
- package/src/constants/common.ts +39 -1
- package/src/constants/constants.ts +9 -8
- package/src/crawlers/commonCrawlerFunc.ts +95 -220
- package/src/crawlers/crawlDomain.ts +10 -23
- package/src/crawlers/crawlLocalFile.ts +2 -0
- package/src/crawlers/crawlSitemap.ts +6 -4
- package/src/crawlers/custom/escapeCssSelector.ts +10 -0
- package/src/crawlers/custom/evaluateAltText.ts +13 -0
- package/src/crawlers/custom/extractAndGradeText.ts +0 -2
- package/src/crawlers/custom/extractText.ts +28 -0
- package/src/crawlers/custom/findElementByCssSelector.ts +46 -0
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +982 -842
- package/src/crawlers/custom/framesCheck.ts +51 -0
- package/src/crawlers/custom/getAxeConfiguration.ts +126 -0
- package/src/crawlers/custom/gradeReadability.ts +30 -0
- package/src/crawlers/custom/xPathToCss.ts +178 -0
- package/src/crawlers/pdfScanFunc.ts +67 -26
- package/src/mergeAxeResults.ts +535 -132
- package/src/npmIndex.ts +130 -62
- package/src/screenshotFunc/htmlScreenshotFunc.ts +1 -1
- package/src/screenshotFunc/pdfScreenshotFunc.ts +34 -1
- package/src/static/ejs/partials/components/ruleOffcanvas.ejs +1 -1
- package/src/static/ejs/partials/components/scanAbout.ejs +1 -1
- package/src/static/ejs/partials/footer.ejs +3 -3
- package/src/static/ejs/partials/scripts/reportSearch.ejs +112 -74
- package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +2 -2
- package/src/static/ejs/partials/summaryMain.ejs +3 -3
- package/src/static/ejs/report.ejs +3 -3
- package/src/utils.ts +289 -13
- package/src/xPathToCssCypress.ts +178 -0
- package/src/crawlers/customAxeFunctions.ts +0 -82
@@ -1,317 +1,366 @@
|
|
1
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
45
|
-
|
46
|
-
if
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
79
|
-
|
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
|
-
|
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
|
-
|
86
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
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
|
-
|
102
|
-
|
103
|
-
return
|
148
|
+
function isValidUnicode(text: string) {
|
149
|
+
if (typeof text !== 'string') {
|
150
|
+
return false;
|
104
151
|
}
|
105
152
|
|
106
|
-
|
107
|
-
|
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
|
-
|
134
|
-
|
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
|
-
|
146
|
-
|
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
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
-
|
176
|
-
|
177
|
-
return summary && hasAccessibleLabel(summary);
|
178
|
-
}
|
195
|
+
return false; // Return false if no valid content is found
|
196
|
+
}
|
179
197
|
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
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
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
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
|
-
|
213
|
-
|
214
|
-
if (
|
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
|
-
|
222
|
-
|
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
|
-
|
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
|
-
|
248
|
-
|
249
|
-
|
263
|
+
// Check the parent element
|
264
|
+
const parent = element.parentElement;
|
265
|
+
if (parent && hasAccessibleLabel(parent)) {
|
250
266
|
return true;
|
251
|
-
|
267
|
+
}
|
252
268
|
|
253
|
-
|
254
|
-
|
255
|
-
hasAccessibleLabel(element) ||
|
256
|
-
isAccessibleText(element.textContent) ||
|
257
|
-
hasCSSContent(element)
|
258
|
-
) {
|
259
|
-
return true;
|
260
|
-
}
|
269
|
+
return false;
|
270
|
+
}
|
261
271
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
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
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
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
|
-
|
313
|
-
|
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 &&
|
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
|
-
|
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
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
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
|
-
|
410
|
+
|
411
|
+
return false; // No visible and focusable child found
|
412
|
+
}
|
358
413
|
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
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
|
-
|
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
|
-
|
380
|
-
|
381
|
-
|
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
|
-
|
443
|
+
function isAccessibleContent(value) {
|
386
444
|
if (!value || value === 'none' || value === 'normal') {
|
387
|
-
|
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
|
-
|
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
|
-
|
400
|
-
|
401
|
-
|
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
|
-
|
404
|
-
|
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
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
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
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
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
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
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
|
-
|
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
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
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
|
-
|
514
|
-
|
515
|
-
|
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
|
522
|
-
|
523
|
-
|
524
|
-
|
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
|
-
//
|
529
|
-
if (
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
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
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
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 (
|
583
|
-
|
584
|
-
|
609
|
+
|
610
|
+
if (parents.children.length > 1)
|
611
|
+
{
|
612
|
+
tracedBackedLayerCount++;
|
585
613
|
}
|
586
614
|
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
return false;
|
591
|
-
}
|
615
|
+
parents = parents.parentElement;
|
616
|
+
}
|
617
|
+
|
592
618
|
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
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
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
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
|
-
|
610
|
-
|
611
|
-
|
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
|
666
|
+
// Check if the element was found
|
667
|
+
if (referencedElement &&
|
668
|
+
(hasAccessibleLabel(referencedElement) ||
|
669
|
+
isAccessibleText(getTextContent(referencedElement)) ||
|
670
|
+
hasAllChildrenAccessible(referencedElement) ))
|
654
671
|
{
|
655
|
-
|
656
|
-
return false;
|
672
|
+
shouldNotFlag = true;
|
657
673
|
}
|
674
|
+
});
|
658
675
|
|
659
|
-
|
660
|
-
|
661
|
-
return
|
676
|
+
if (shouldNotFlag)
|
677
|
+
{
|
678
|
+
return false
|
662
679
|
}
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
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
|
-
//
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
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
|
-
|
681
|
-
|
682
|
-
customConsoleWarn("
|
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
|
-
//
|
687
|
-
|
688
|
-
|
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
|
-
|
693
|
-
|
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
|
-
|
698
|
-
|
699
|
-
);
|
700
|
-
if (
|
701
|
-
customConsoleWarn("
|
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
|
-
|
706
|
-
|
707
|
-
|
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
|
-
//
|
711
|
-
|
712
|
-
|
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
|
717
|
-
|
718
|
-
|
719
|
-
|
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 (
|
725
|
-
|
726
|
-
|
727
|
-
|
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
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
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
|
-
|
783
|
-
|
784
|
-
|
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.
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
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
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
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
|
-
|
862
|
-
|
863
|
-
|
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
|
-
|
869
|
-
|
870
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
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
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
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
|
-
|
909
|
-
|
910
|
-
|
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
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
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
|
-
|
973
|
-
|
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
|
-
|
976
|
-
|
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
|
-
|
981
|
-
|
982
|
-
|
983
|
-
|
984
|
-
|
985
|
-
const
|
986
|
-
|
987
|
-
const
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
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
|
-
|
995
|
-
|
996
|
-
|
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
|
-
|
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
|
-
|
1013
|
-
|
1160
|
+
// Update previousFlaggedXPathsByDocument before finishing
|
1161
|
+
previousFlaggedXPathsByDocument = { ...flaggedXPathsByDocument };
|
1014
1162
|
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1163
|
+
cleanupFlaggedElements();
|
1164
|
+
return previousAllFlaggedElementsXPaths;
|
1165
|
+
}
|
1018
1166
|
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
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
|
-
|
1035
|
-
});
|
1175
|
+
return flagElements();
|
1036
1176
|
};
|