@empiricalrun/test-gen 0.42.4 → 0.42.8

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.
@@ -1,491 +0,0 @@
1
- /* eslint-disable no-undef */
2
- /**
3
- * Annotates all clickable elements on the page with unique hint markers.
4
- * Returns an object containing annotations and methods to enable/disable them.
5
- *
6
- * @param {Object} options - Configuration options for hint markers.
7
- * @param {string} options.hintCharacterSet - Characters to use for generating hint identifiers.
8
- * @param {number} options.maxHints - Maximum number of hints to generate.
9
- * @param {string} options.markerClass - CSS class to apply to hint markers.
10
- * @returns {Object} An object containing annotations map and enable/disable methods.
11
- */
12
- function annotateClickableElements({ options = {}, preference } = {}) {
13
- const {
14
- hintCharacterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", // Default set of characters for hints
15
- maxHints = 1000, // Maximum number of hints to generate
16
- markerClass = "hint-marker", // CSS class for markers
17
- } = options;
18
-
19
- const document = window.document;
20
- const annotationsMap = {};
21
- const usedHints = new Set();
22
- let annotationsContainer = null;
23
-
24
- // Check if the element is not blocked and visible for clicking
25
- function isElementClickNotBlocked(element, windowToAnnotate) {
26
- const rect = element.getBoundingClientRect();
27
- const originalScrollX = windowToAnnotate.scrollX;
28
- const originalScrollY = windowToAnnotate.scrollY;
29
-
30
- // Calculate the center point of the element
31
- const centerX = rect.left + rect.width / 2;
32
- const centerY = rect.top + rect.height / 2;
33
-
34
- // check if element is within the viewport
35
- if (
36
- centerX < 0 ||
37
- centerY < 0 ||
38
- centerX > (windowToAnnotate.innerWidth || document.documentElement.clientWidth) ||
39
- centerY > (windowToAnnotate.innerHeight || document.documentElement.clientHeight)
40
- ) {
41
- // Determine the viewport dimensions
42
- const viewportWidth =
43
- windowToAnnotate.innerWidth || document.documentElement.clientWidth;
44
- const viewportHeight =
45
- windowToAnnotate.innerHeight || document.documentElement.clientHeight;
46
-
47
- // Calculate the new scroll positions to bring the element into the center of the viewport
48
- const newScrollX = centerX - viewportWidth / 2;
49
- const newScrollY = centerY - viewportHeight / 2;
50
-
51
- // Scroll the window to the new positions
52
- windowToAnnotate.scrollTo({
53
- top: newScrollY,
54
- left: newScrollX,
55
- });
56
-
57
- const newRect = element.getBoundingClientRect();
58
- const newCenterX = newRect.left + newRect.width / 2;
59
- const newCenterY = newRect.top + newRect.height / 2;
60
- const topElement = document.elementFromPoint(newCenterX, newCenterY);
61
- const doesElementContainTopElement = element.contains(topElement);
62
-
63
-
64
- // Restore the original scroll positions
65
- windowToAnnotate.scrollTo(originalScrollX, originalScrollY);
66
- return doesElementContainTopElement;
67
- }
68
-
69
- const topElement = windowToAnnotate.document.elementFromPoint(centerX, centerY);
70
-
71
- // Check if the topmost element is the target element or one of its descendants
72
- return element.contains(topElement);
73
- }
74
-
75
- function generateHintStrings(charset, max) {
76
- const hints = [];
77
- let length = 1;
78
-
79
- while (hints.length < max) {
80
- const combos = cartesianProduct(Array(length).fill(charset.split("")));
81
- for (const combo of combos) {
82
- const hint = combo.join("");
83
- if (!usedHints.has(hint)) {
84
- hints.push(hint);
85
- usedHints.add(hint);
86
- }
87
- if (hints.length >= max) break;
88
- }
89
- length++;
90
- }
91
-
92
- return hints;
93
- }
94
-
95
- function cartesianProduct(arrays) {
96
- return arrays.reduce(
97
- (acc, curr) =>
98
- acc
99
- .map((a) => curr.map((b) => a.concat([b])))
100
- .reduce((a, b) => a.concat(b), []),
101
- [[]]
102
- );
103
- }
104
-
105
- // Check if an element is clickable
106
- function isElementClickable(element, windowToAnnotate) {
107
- if (!(element instanceof windowToAnnotate.Element)) return false;
108
-
109
- const tagName = element.tagName.toLowerCase();
110
- let isClickable = false;
111
-
112
- // Check for aria-disabled
113
- const ariaDisabled = element.getAttribute("aria-disabled");
114
- if (ariaDisabled && ["", "true"].includes(ariaDisabled.toLowerCase())) {
115
- return false; // Element should not be clickable if aria-disabled is true
116
- }
117
-
118
- // Check for visibility
119
- const style = window.getComputedStyle(element);
120
- if (
121
- style.display === "none" ||
122
- style.visibility === "hidden" ||
123
- // This is done for cases where opacity is undefined
124
- // parseFloat(style.opacity) === 0
125
- style.pointerEvents === "none"
126
- ) {
127
- return false;
128
- }
129
-
130
- // Check if element is disabled (for applicable elements)
131
- if (element.disabled) return false;
132
-
133
- // Check for AngularJS click handlers
134
- if (!isElementClickable._checkForAngularJs) {
135
- isElementClickable._checkForAngularJs = (function () {
136
- const angularElements = document.getElementsByClassName("ng-scope");
137
- if (angularElements.length === 0) {
138
- return () => false;
139
- } else {
140
- const ngAttributes = [];
141
- for (const prefix of ["", "data-", "x-"]) {
142
- for (const separator of ["-", ":", "_"]) {
143
- ngAttributes.push(`${prefix}ng${separator}click`);
144
- }
145
- }
146
- return function (el) {
147
- for (const attribute of ngAttributes) {
148
- if (el.hasAttribute(attribute)) return true;
149
- }
150
- return false;
151
- };
152
- }
153
- })();
154
- }
155
-
156
- if (!isClickable && isElementClickable._checkForAngularJs(element)) {
157
- isClickable = true;
158
- }
159
-
160
- // Tag-specific clickability
161
- const focusableTags = [
162
- "a",
163
- "button",
164
- "input",
165
- "select",
166
- "textarea",
167
- "object",
168
- "embed",
169
- "label",
170
- "details",
171
- ];
172
-
173
- // Check for onclick attribute or listener
174
- if (
175
- (element.hasAttribute("onclick") ||
176
- typeof element.onclick === "function")
177
- // check for parent element having same on click
178
- && element.parentNode.onclick !== element.onclick
179
- // parent element should not be a focusable tag like button
180
- && focusableTags.indexOf(element.parentNode.tagName.toLowerCase()) === -1
181
- ) {
182
- isClickable = true;
183
- }
184
-
185
- // Check for jsaction attribute (commonly used in frameworks like Google's)
186
- if (!isClickable && element.hasAttribute("jsaction")) {
187
- const jsactionRules = element.getAttribute("jsaction").split(";");
188
- for (const jsactionRule of jsactionRules) {
189
- const ruleSplit = jsactionRule.trim().split(":");
190
- if (ruleSplit.length >= 1 && ruleSplit.length <= 2) {
191
- const [eventType] = ruleSplit[0].trim().split(".");
192
- if (eventType === "click") {
193
- isClickable = true;
194
- break;
195
- }
196
- }
197
- }
198
- }
199
-
200
- // Check for role attributes that imply clickability
201
- if (!isClickable) {
202
- const role = element.getAttribute("role");
203
- const clickableRoles = [
204
- "button",
205
- "tab",
206
- "link",
207
- "checkbox",
208
- "menuitem",
209
- "menuitemcheckbox",
210
- "menuitemradio",
211
- "radio",
212
- "switch",
213
- ];
214
- if (role && clickableRoles.includes(role.toLowerCase())) {
215
- isClickable = true;
216
- }
217
- }
218
-
219
- // Check for contentEditable
220
- if (!isClickable) {
221
- const contentEditable = element.getAttribute("contentEditable");
222
- if (
223
- contentEditable != null &&
224
- ["", "contenteditable", "true"].includes(contentEditable.toLowerCase())
225
- ) {
226
- isClickable = true;
227
- }
228
- }
229
-
230
-
231
- if (focusableTags.includes(tagName)) {
232
- switch (tagName) {
233
- case "a":
234
- // Ensure it's not just a named anchor without href
235
- if (element.hasAttribute("href")) {
236
- isClickable = true;
237
- }
238
- break;
239
- case "input": {
240
- const type = (element.getAttribute("type") || "").toLowerCase();
241
- if (
242
- type !== "hidden" &&
243
- !element.disabled &&
244
- !(element.readOnly && isInputSelectable(element))
245
- ) {
246
- isClickable = true;
247
- }
248
- break;
249
- }
250
- case "textarea":
251
- if (!element.disabled && !element.readOnly) {
252
- isClickable = true;
253
- }
254
- break;
255
- case "button":
256
- case "select":
257
- if (!element.disabled) {
258
- isClickable = true;
259
- }
260
- break;
261
- case "object":
262
- case "embed":
263
- isClickable = true;
264
- break;
265
- case "label":
266
- if (element.control && !element.control.disabled) {
267
- isClickable = true;
268
- }
269
- break;
270
- case "details":
271
- isClickable = true;
272
- break;
273
- default:
274
- break;
275
- }
276
- }
277
-
278
- // Check for class names containing 'button' as a possible click target
279
- if (!isClickable) {
280
- const className = element.getAttribute("class");
281
- if (className && className.toLowerCase().includes("button")) {
282
- isClickable = true;
283
- } else if (element.classList.contains("cursor-pointer")) {
284
- isClickable = true;
285
- } else if (element.classList.contains("v-list-item--link")) {
286
- // vue specific click handling
287
- isClickable = true;
288
- } else if (element.style.cursor === "pointer") {
289
- isClickable = true;
290
- }
291
- }
292
-
293
- // Check for tabindex
294
- if (!isClickable) {
295
- const tabIndexValue = element.getAttribute("tabindex");
296
- const tabIndex = tabIndexValue ? parseInt(tabIndexValue) : -1;
297
- if (tabIndex >= 0 && !isNaN(tabIndex)) {
298
- isClickable = true;
299
- }
300
- }
301
-
302
- return isClickable;
303
- }
304
-
305
- function isInputSelectable(input) {
306
- const selectableTypes = [
307
- "text",
308
- "search",
309
- "password",
310
- "url",
311
- "email",
312
- "number",
313
- "tel",
314
- ];
315
- const type = (input.getAttribute("type") || "").toLowerCase();
316
- return selectableTypes.includes(type);
317
- }
318
-
319
- var parentElements = [];
320
-
321
- // Create a hint marker
322
- function createHintMarker(el, hint, parentElement, windowToAnnotate) {
323
- const rect = el.getBoundingClientRect();
324
-
325
- // Create the marker element
326
- const marker = document.createElement("div");
327
- marker.textContent = hint;
328
- marker.className = markerClass;
329
- // Style the marker
330
- Object.assign(marker.style, {
331
- position: "absolute",
332
- top: `${rect.top + windowToAnnotate.scrollY}px`,
333
- left: `${rect.left + windowToAnnotate.scrollX}px`,
334
- background:
335
- "-webkit-gradient(linear, 0% 0%, 0% 100%, from(rgb(255, 247, 133)), to(rgb(255, 197, 66)))",
336
- padding: "1px 3px 0px",
337
- borderRadius: "3px",
338
- border: "1px solid rgb(227, 190, 35)",
339
- fontSize: "11px",
340
- pointerEvents: "none",
341
- zIndex: "10000",
342
- whiteSpace: "nowrap",
343
- overflow: "hidden",
344
- boxShadow: "rgba(0, 0, 0, 0.3) 0px 3px 7px 0px",
345
- letterSpacing: 0,
346
- minHeight: 0,
347
- lineHeight: "100%",
348
- color: "rgb(48, 37, 5)",
349
- fontFamily: "Helvetica, Arial, sans-serif",
350
- fontWeight: "bold",
351
- textShadow: "rgba(255, 255, 255, 0.6) 0px 1px 0px",
352
- });
353
-
354
- // Attach the marker to the specified parent element
355
- parentElement.appendChild(marker);
356
- parentElements.push(parentElement);
357
- return marker;
358
- }
359
-
360
-
361
- // Clear existing annotations
362
- //TODO: Handle clearing annotations
363
- function clearAnnotations() {
364
- parentElements.forEach((parentElement) => {
365
- const markers = parentElement.querySelectorAll(`.${markerClass}`);
366
- markers.forEach((marker) => marker.remove());
367
- });
368
- parentElements = [];
369
- }
370
-
371
- function isInputElement(el) {
372
- // Check if it's an input with a text-like type
373
- if (el instanceof HTMLInputElement) {
374
- const textTypes = ['text', 'email', 'number', 'password', 'search', 'tel', 'url', 'checkbox'];
375
- return textTypes.includes(el.type.toLowerCase());
376
- }
377
-
378
- // Check if it's a textarea
379
- if (el instanceof HTMLTextAreaElement) {
380
- return true;
381
- }
382
-
383
- // Check if it's contentEditable
384
- return el.isContentEditable;
385
- }
386
-
387
- // Initialize annotations for a given window (including iframes)
388
- function initializeAnnotations(windowToAnnotate, parentHints, depth) {
389
- const container =
390
- parentHints?.nodeName === "IFRAME"
391
- ? parentHints.contentWindow.document.body
392
- : annotationsContainer;
393
-
394
- // Ensure the container exists
395
- if (!container) return;
396
-
397
- // Filter for clickable elements
398
- const clickableElements = Array.from(windowToAnnotate.document.querySelectorAll("*")).filter((el) => {
399
-
400
- //If preference is fill then it should only annotate input elements
401
- if(preference === "fill"){
402
- if (!isInputElement(el)) {
403
- return false;
404
- }
405
- }
406
-
407
- const isClickable = isElementClickable(el, windowToAnnotate);
408
- const isClickNotBlocked = isElementClickNotBlocked(el, windowToAnnotate);
409
- return isClickable && isClickNotBlocked;
410
- });
411
- // Generate hint strings for the clickable elements
412
- const hints = generateHintStrings(hintCharacterSet, Math.min(maxHints, clickableElements.length));
413
-
414
- // Create markers for the elements
415
- clickableElements.slice(0, maxHints).forEach((el, index) => {
416
- const hint = hints[index];
417
- const rect = el.getBoundingClientRect();
418
-
419
- // Use createHintMarker with the specified container
420
- createHintMarker(el, hint, container, windowToAnnotate);
421
- el.style.boxShadow = `inset 0 0 0px 2px red`;
422
-
423
- // Add element details to the annotations map
424
- annotationsMap[hint] = {
425
- node: el,
426
- rect: {
427
- top: rect.top + windowToAnnotate.scrollY,
428
- left: rect.left + windowToAnnotate.scrollX,
429
- width: rect.width,
430
- height: rect.height,
431
- },
432
- depth: [...depth ]
433
- };
434
- });
435
-
436
- // Process iframes recursively
437
- Array.from(windowToAnnotate.document.querySelectorAll("iframe")).forEach((iframe) => {
438
- try {
439
- const frameWindow = iframe.contentWindow;
440
- if (frameWindow && iframe.offsetWidth > 0 && iframe.offsetHeight > 0) {
441
- initializeAnnotations(frameWindow, iframe, [...depth, iframe]);
442
- }
443
- } catch (e) {
444
- console.warn("Cannot access iframe:", e);
445
- }
446
- });
447
- }
448
-
449
-
450
- // Initialize and enable annotations
451
- function enable() {
452
- clearAnnotations();
453
- if (!annotationsContainer) {
454
- annotationsContainer = document.createElement("div");
455
- annotationsContainer.className = "annotations";
456
- Object.assign(annotationsContainer.style, {
457
- position: "absolute",
458
- top: "0",
459
- left: "0",
460
- width: "100%",
461
- height: "100%",
462
- pointerEvents: "none",
463
- zIndex: "9999",
464
- });
465
- document.body.appendChild(annotationsContainer);
466
- initializeAnnotations(window, null, []);
467
- } else {
468
- annotationsContainer.style.display = "block";
469
- }
470
- }
471
-
472
- // Destroy annotations
473
- function destroy() {
474
- if (annotationsContainer) {
475
- clearAnnotations();
476
- Object.values(annotationsMap).forEach((annotation) => {
477
- annotation.node.style.boxShadow = "none";
478
- });
479
-
480
- annotationsContainer.parentNode.removeChild(annotationsContainer);
481
- annotationsContainer = null;
482
- }
483
- }
484
-
485
- enable();
486
-
487
- return {
488
- annotations: annotationsMap,
489
- destroy,
490
- };
491
- }