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