@empiricalrun/test-gen 0.38.5 → 0.38.7

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.7
4
+
5
+ ### Patch Changes
6
+
7
+ - b13b57d: fix: remove duplicate code
8
+
9
+ ## 0.38.6
10
+
11
+ ### Patch Changes
12
+
13
+ - 72c09d9: feat: added support for annotations in iframe
14
+
3
15
  ## 0.38.5
4
16
 
5
17
  ### Patch Changes
@@ -9,28 +9,20 @@
9
9
  * @param {string} options.markerClass - CSS class to apply to hint markers.
10
10
  * @returns {Object} An object containing annotations map and enable/disable methods.
11
11
  */
12
- window.annotateClickableElements = function annotateClickableElements(
13
- options = {},
14
- ) {
12
+ function annotateClickableElements(options = {}) {
15
13
  const {
16
14
  hintCharacterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", // Default set of characters for hints
17
15
  maxHints = 1000, // Maximum number of hints to generate
18
16
  markerClass = "hint-marker", // CSS class for markers
19
17
  } = options;
20
18
 
19
+ const document = window.document;
21
20
  const annotationsMap = {};
21
+ const usedHints = new Set();
22
22
  let annotationsContainer = null;
23
23
 
24
- /**
25
- * Checks if the provided DOM element is clickable by ensuring that
26
- * no other elements are covering it, regardless of its current
27
- * viewport visibility.
28
- *
29
- * @param {Element} element - The DOM element to check.
30
- * @returns {boolean} - Returns true if the element is clickable; otherwise, false.
31
- */
32
-
33
- function isElementClickNotBlocked(element) {
24
+ // Check if the element is not blocked and visible for clicking
25
+ function isElementClickNotBlocked(element, windowToAnnotate) {
34
26
  const rect = element.getBoundingClientRect();
35
27
 
36
28
  // Calculate the center point of the element
@@ -41,21 +33,21 @@ window.annotateClickableElements = function annotateClickableElements(
41
33
  if (
42
34
  centerX < 0 ||
43
35
  centerY < 0 ||
44
- centerX > (window.innerWidth || document.documentElement.clientWidth) ||
45
- centerY > (window.innerHeight || document.documentElement.clientHeight)
36
+ centerX > (windowToAnnotate.innerWidth || document.documentElement.clientWidth) ||
37
+ centerY > (windowToAnnotate.innerHeight || document.documentElement.clientHeight)
46
38
  ) {
47
39
  // Determine the viewport dimensions
48
40
  const viewportWidth =
49
- window.innerWidth || document.documentElement.clientWidth;
41
+ windowToAnnotate.innerWidth || document.documentElement.clientWidth;
50
42
  const viewportHeight =
51
- window.innerHeight || document.documentElement.clientHeight;
43
+ windowToAnnotate.innerHeight || document.documentElement.clientHeight;
52
44
 
53
45
  // Calculate the new scroll positions to bring the element into the center of the viewport
54
46
  const newScrollX = centerX - viewportWidth / 2;
55
47
  const newScrollY = centerY - viewportHeight / 2;
56
48
 
57
49
  // Scroll the window to the new positions
58
- window.scrollTo({
50
+ windowToAnnotate.scrollTo({
59
51
  top: newScrollY,
60
52
  left: newScrollX,
61
53
  });
@@ -67,20 +59,12 @@ window.annotateClickableElements = function annotateClickableElements(
67
59
  return element.contains(topElement);
68
60
  }
69
61
 
70
- // Retrieve the topmost element at the center point
71
- const topElement = document.elementFromPoint(centerX, centerY);
62
+ const topElement = windowToAnnotate.document.elementFromPoint(centerX, centerY);
72
63
 
73
64
  // Check if the topmost element is the target element or one of its descendants
74
65
  return element.contains(topElement);
75
66
  }
76
67
 
77
- /**
78
- * Generates a sequence of hint strings based on the provided character set.
79
- * For example, with 'abc' as the set and maxHints 5, it generates: 'a', 'b', 'c', 'aa', 'ab'
80
- * @param {string} charset - Characters to use.
81
- * @param {number} max - Maximum number of hints to generate.
82
- * @returns {string[]} Array of hint strings.
83
- */
84
68
  function generateHintStrings(charset, max) {
85
69
  const hints = [];
86
70
  let length = 1;
@@ -88,7 +72,11 @@ window.annotateClickableElements = function annotateClickableElements(
88
72
  while (hints.length < max) {
89
73
  const combos = cartesianProduct(Array(length).fill(charset.split("")));
90
74
  for (const combo of combos) {
91
- hints.push(combo.join(""));
75
+ const hint = combo.join("");
76
+ if (!usedHints.has(hint)) {
77
+ hints.push(hint);
78
+ usedHints.add(hint);
79
+ }
92
80
  if (hints.length >= max) break;
93
81
  }
94
82
  length++;
@@ -97,29 +85,19 @@ window.annotateClickableElements = function annotateClickableElements(
97
85
  return hints;
98
86
  }
99
87
 
100
- /**
101
- * Creates a Cartesian product of arrays.
102
- * @param {Array<Array>} arrays - Array of arrays for Cartesian product.
103
- * @returns {Array<Array>} Cartesian product.
104
- */
105
88
  function cartesianProduct(arrays) {
106
89
  return arrays.reduce(
107
90
  (acc, curr) =>
108
91
  acc
109
92
  .map((a) => curr.map((b) => a.concat([b])))
110
93
  .reduce((a, b) => a.concat(b), []),
111
- [[]],
94
+ [[]]
112
95
  );
113
96
  }
114
97
 
115
- /**
116
- * Checks if an element is visible and interactable.
117
- * Enhanced version inspired by getLocalHintsForElement.
118
- * @param {Element} element - DOM element.
119
- * @returns {boolean} True if the element is clickable and visible.
120
- */
98
+ // Check if an element is clickable
121
99
  function isElementClickable(element) {
122
- if (!(element instanceof Element)) return false;
100
+ // if (!(element instanceof Element)) return false;
123
101
 
124
102
  const tagName = element.tagName.toLowerCase();
125
103
  let isClickable = false;
@@ -311,11 +289,6 @@ window.annotateClickableElements = function annotateClickableElements(
311
289
  return isClickable;
312
290
  }
313
291
 
314
- /**
315
- * Helper function to determine if an input is selectable (e.g., text, search).
316
- * @param {Element} input - The input element.
317
- * @returns {boolean} True if the input type is selectable.
318
- */
319
292
  function isInputSelectable(input) {
320
293
  const selectableTypes = [
321
294
  "text",
@@ -330,68 +303,23 @@ window.annotateClickableElements = function annotateClickableElements(
330
303
  return selectableTypes.includes(type);
331
304
  }
332
305
 
333
- /**
334
- * Generates a unique CSS selector path for a given element.
335
- * @param {Element} element - The DOM element.
336
- * @returns {string} A unique CSS selector string.
337
- */
338
- function getUniqueSelector(element) {
339
- if (element.id) {
340
- return `#${CSS.escape(element.id)}`;
341
- }
342
-
343
- const parts = [];
344
- while (element && element.nodeType === Node.ELEMENT_NODE) {
345
- let part = element.nodeName.toLowerCase();
346
-
347
- // Use classList instead of className to avoid errors
348
- if (element.classList && element.classList.length > 0) {
349
- const classes = Array.from(element.classList)
350
- .filter((cls) => cls.length > 0)
351
- .map((cls) => `.${CSS.escape(cls)}`)
352
- .join("");
353
- part += classes;
354
- }
355
-
356
- // Add nth-child if necessary
357
- const parent = element.parentNode;
358
- if (parent) {
359
- const siblings = Array.from(parent.children).filter(
360
- (sibling) => sibling.nodeName === element.nodeName,
361
- );
362
- if (siblings.length > 1) {
363
- const index =
364
- Array.prototype.indexOf.call(parent.children, element) + 1;
365
- part += `:nth-child(${index})`;
366
- }
367
- }
368
-
369
- parts.unshift(part);
370
- element = element.parentElement;
371
- }
372
-
373
- return parts.join(" > ");
374
- }
306
+ var parentElements = [];
375
307
 
376
- /**
377
- * Creates a hint marker element positioned at the top-left of the target element.
378
- * @param {Element} el - The clickable DOM element.
379
- * @param {string} hint - The hint identifier.
380
- * @param {string} nodePath - Unique CSS selector path for the element.
381
- * @returns {HTMLElement} The created hint marker element.
382
- */
383
- function createHintMarker(el, hint) {
308
+ // Create a hint marker
309
+ function createHintMarker(el, hint, parentElement) {
384
310
  const rect = el.getBoundingClientRect();
385
-
311
+ const parentRect = parentElement.getBoundingClientRect();
312
+
313
+ // Create the marker element
386
314
  const marker = document.createElement("div");
387
315
  marker.textContent = hint;
388
316
  marker.className = markerClass;
389
-
317
+
390
318
  // Style the marker
391
319
  Object.assign(marker.style, {
392
320
  position: "absolute",
393
- top: `${rect.top + window.scrollY}px`,
394
- left: `${rect.left + window.scrollX}px`,
321
+ top: `${rect.top + parentRect.top}px`,
322
+ left: `${rect.left + parentRect.left}px`,
395
323
  background:
396
324
  "-webkit-gradient(linear, 0% 0%, 0% 100%, from(rgb(255, 247, 133)), to(rgb(255, 197, 66)))",
397
325
  padding: "1px 3px 0px",
@@ -411,116 +339,116 @@ window.annotateClickableElements = function annotateClickableElements(
411
339
  fontWeight: "bold",
412
340
  textShadow: "rgba(255, 255, 255, 0.6) 0px 1px 0px",
413
341
  });
414
-
415
- annotationsContainer.appendChild(marker);
416
-
342
+
343
+ // Attach the marker to the specified parent element
344
+ parentElement.appendChild(marker);
345
+ parentElements.push(parentElement);
417
346
  return marker;
418
347
  }
348
+
419
349
 
420
- /**
421
- * Clears all existing hint markers from the annotations container.
422
- */
350
+ // Clear existing annotations
351
+ //TODO: Handle clearing annotations
423
352
  function clearAnnotations() {
424
- if (annotationsContainer) {
425
- annotationsContainer.remove();
426
- annotationsContainer = null;
427
- }
428
- }
429
-
430
- /**
431
- * Initializes the annotations by creating the container and adding markers.
432
- */
433
- function initializeAnnotations() {
434
- clearAnnotations(); // Clear any existing annotations
435
-
436
- annotationsContainer = document.createElement("div");
437
- annotationsContainer.className = "annotations";
438
- // Ensure the container covers the entire page
439
- Object.assign(annotationsContainer.style, {
440
- position: "absolute",
441
- top: "0",
442
- left: "0",
443
- width: "100%",
444
- height: "100%",
445
- pointerEvents: "none", // Allow clicks to pass through
446
- zIndex: "9999", // Ensure it's above other elements
353
+ parentElements.forEach((parentElement) => {
354
+ const markers = parentElement.querySelectorAll(`.${markerClass}`);
355
+ markers.forEach((marker) => marker.remove());
447
356
  });
448
- document.body.appendChild(annotationsContainer);
449
-
450
- const clickableElements = Array.from(document.querySelectorAll("*")).filter(
451
- (el) => {
452
- const isClickable = isElementClickable(el);
453
-
454
- const originalScrollX = window.scrollX;
455
- const originalScrollY = window.scrollY;
456
-
457
- const isClickNotBlocked = isElementClickNotBlocked(el);
458
- // Restore the original scroll positions
459
- window.scrollTo(originalScrollX, originalScrollY);
460
-
461
- return isClickable && isClickNotBlocked;
462
- },
463
- );
464
- const hints = generateHintStrings(
465
- hintCharacterSet,
466
- Math.min(maxHints, clickableElements.length),
467
- );
357
+ parentElements = [];
358
+ }
468
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
469
378
  clickableElements.slice(0, maxHints).forEach((el, index) => {
470
379
  const hint = hints[index];
471
- const nodePath = getUniqueSelector(el);
472
380
  const rect = el.getBoundingClientRect();
473
-
474
- // Create the hint marker
475
- createHintMarker(el, hint, nodePath);
381
+
382
+ // Use createHintMarker with the specified container
383
+ createHintMarker(el, hint, container);
476
384
  el.style.boxShadow = `inset 0 0 0px 2px red`;
477
385
 
478
- // Populate the annotations map
386
+ // Add element details to the annotations map
479
387
  annotationsMap[hint] = {
480
388
  node: el,
481
- nodePath: nodePath,
482
389
  rect: {
483
- top: rect.top + window.scrollY,
484
- left: rect.left + window.scrollX,
390
+ top: rect.top + windowToAnnotate.scrollY,
391
+ left: rect.left + windowToAnnotate.scrollX,
485
392
  width: rect.width,
486
393
  height: rect.height,
487
- right: rect.right + window.scrollX,
488
- bottom: rect.bottom + window.scrollY,
489
394
  },
395
+ depth: [...depth ]
490
396
  };
491
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
+ });
492
410
  }
411
+
493
412
 
494
- /**
495
- * Enables the annotations by making the annotations container visible.
496
- */
413
+ // Initialize and enable annotations
497
414
  function enable() {
415
+ clearAnnotations();
498
416
  if (!annotationsContainer) {
499
- initializeAnnotations();
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, []);
500
430
  } else {
501
431
  annotationsContainer.style.display = "block";
502
432
  }
503
433
  }
504
434
 
505
- /**
506
- * removes all generated hints from DOM
507
- */
435
+ // Destroy annotations
508
436
  function destroy() {
509
437
  if (annotationsContainer) {
438
+ clearAnnotations();
510
439
  Object.values(annotationsMap).forEach((annotation) => {
511
440
  annotation.node.style.boxShadow = "none";
512
441
  });
513
442
 
514
443
  annotationsContainer.parentNode.removeChild(annotationsContainer);
444
+ annotationsContainer = null;
515
445
  }
516
446
  }
517
447
 
518
- // Initialize annotations upon first run
519
448
  enable();
520
449
 
521
- // Return the desired object
522
450
  return {
523
451
  annotations: annotationsMap,
524
452
  destroy,
525
453
  };
526
- };
454
+ }
@@ -15,7 +15,7 @@ test("should annotate all links on empirical landing page", async ({
15
15
  });
16
16
 
17
17
  const annotations = await page.evaluate(() => {
18
- const { annotations } = window.annotateClickableElements();
18
+ const { annotations } = annotateClickableElements();
19
19
 
20
20
  return Object.entries(annotations).map(([hint, config]) => ({
21
21
  innerText: config.node.innerText,
@@ -80,7 +80,7 @@ test("should annotate all important items on quizizz page", async ({
80
80
  });
81
81
 
82
82
  const annotations = await page.evaluate(() => {
83
- const { annotations } = window.annotateClickableElements();
83
+ const { annotations } = annotateClickableElements();
84
84
 
85
85
  return Object.entries(annotations).map(([hint, config]) => ({
86
86
  hint,
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/actions/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,UAAU,EAAE,GAAG,CAAC;QAChB,MAAM,EAAE,GAAG,CAAC;KACb;CACF;AAED,wBAAsB,oCAAoC,CACxD,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,IAAI,EACV,iBAAiB,CAAC,EAAE,MAAM,gBAoF3B;AAED,wBAAgB,cAAc,WAG7B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/actions/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,UAAU,EAAE,GAAG,CAAC;QAChB,MAAM,EAAE,GAAG,CAAC;KACb;CACF;AAED,wBAAsB,oCAAoC,CACxD,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,IAAI,EACV,iBAAiB,CAAC,EAAE,MAAM,gBAkH3B;AAED,wBAAgB,cAAc,WAG7B"}
@@ -16,6 +16,31 @@ async function getPlaywrightLocatorUsingCssSelector(cssSelector, xpath, page, el
16
16
  selectedElem =
17
17
  // @ts-ignore
18
18
  window?.annotationInstance?.annotations?.[elementAnnotation]?.node;
19
+ const elementDepth =
20
+ //@ts-ignore
21
+ window?.annotationInstance?.annotations?.[elementAnnotation].depth;
22
+ const frameLocatorStr = elementDepth
23
+ // @ts-ignore
24
+ .map((e, index) => {
25
+ const locator = index === 0
26
+ ? //To handle the parent iframe
27
+ window.playwright.generateLocator(e)
28
+ : elementDepth[index - 1].contentWindow.playwright.generateLocator(e);
29
+ return locator.replace(/^locator\(/, "frameLocator(");
30
+ })
31
+ .join(".");
32
+ let elementLocator;
33
+ //If the element is inside an iframe, we need to append the locator for the iframe
34
+ if (elementDepth.length > 0) {
35
+ elementLocator =
36
+ elementDepth[elementDepth.length - 1].contentWindow.playwright.generateLocator(selectedElem);
37
+ }
38
+ else {
39
+ elementLocator = window.playwright.generateLocator(selectedElem);
40
+ }
41
+ return frameLocatorStr
42
+ ? `${frameLocatorStr}.${elementLocator}`
43
+ : elementLocator;
19
44
  }
20
45
  else if (locator.xpath) {
21
46
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/agent/browsing/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAK3D,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAiBvD,OAAO,EAAe,aAAa,EAAE,MAAM,aAAa,CAAC;AAMzD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,MAAM,CAKhD;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,UAIvD;AA6FD;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,aAAa,EACxB,KAAK,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,CAAC,CA0DjB;AAyBD,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,IAAI,iBAgCxD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,QA+BjD;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAM1E;AAWD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,oBAAoB,EACtC,gBAAgB,GAAE,MAAM,EAAU,GACjC,OAAO,CAAC,MAAM,CAAC,CA+CjB;AAED,wBAAsB,sBAAsB,CAAC,EAC3C,YAAiB,EACjB,IAAS,EACT,eAAoB,EACpB,gBAAqB,EACrB,UAAyC,GAC1C,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,8EASA;AAED,qBAAa,eAAe;IACd,OAAO,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM;IACrC,OAAO,CAAC,aAAa,CAAqB;YAE5B,mBAAmB;YAUnB,gBAAgB;IAsBjB,OAAO;IAuBb,SAAS;CAKjB"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/agent/browsing/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAK3D,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAiBvD,OAAO,EAAe,aAAa,EAAE,MAAM,aAAa,CAAC;AAMzD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,MAAM,CAKhD;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,UAIvD;AA6FD;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,aAAa,EACxB,KAAK,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,CAAC,CA0DjB;AAyBD,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,IAAI,iBAuGxD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,QA+BjD;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAM1E;AAWD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,oBAAoB,EACtC,gBAAgB,GAAE,MAAM,EAAU,GACjC,OAAO,CAAC,MAAM,CAAC,CA+CjB;AAED,wBAAsB,sBAAsB,CAAC,EAC3C,YAAiB,EACjB,IAAS,EACT,eAAoB,EACpB,gBAAqB,EACrB,UAAyC,GAC1C,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,8EASA;AAED,qBAAa,eAAe;IACd,OAAO,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM;IACrC,OAAO,CAAC,aAAa,CAAqB;YAE5B,mBAAmB;YAUnB,gBAAgB;IAsBjB,OAAO;IAuBb,SAAS;CAKjB"}
@@ -179,15 +179,83 @@ async function injectPwLocatorGenerator(page) {
179
179
  fs_extra_1.default.readFile(annotateElementPath, "utf-8"),
180
180
  ]);
181
181
  page.on("load", async () => {
182
- // add script for subsequent page load events
183
182
  try {
184
183
  await Promise.all(scripts.map((s) => page.addScriptTag({ content: s })));
184
+ await page.evaluate(async () => {
185
+ //@ts-ignore
186
+ const injectScriptInIframe = (iframeDoc) => {
187
+ try {
188
+ [
189
+ "https://assets-test.empirical.run/pw-selector.js",
190
+ "https://code.jquery.com/jquery-3.7.1.min.js",
191
+ ].forEach((url) => {
192
+ const scr = iframeDoc.createElement("script");
193
+ scr.src = url;
194
+ console.log("Injecting script in iframe", scr);
195
+ iframeDoc.head.appendChild(scr);
196
+ });
197
+ }
198
+ catch (e) {
199
+ console.warn("Error injecting script in iframe:", e);
200
+ }
201
+ };
202
+ const iframes = document.getElementsByTagName("iframe");
203
+ for (const iframe of iframes) {
204
+ iframe.getBoundingClientRect();
205
+ const rect = iframe.getBoundingClientRect();
206
+ const isVisible = rect.width > 0 && rect.height > 0;
207
+ if (isVisible) {
208
+ //@ts-ignore
209
+ const iframeContent = iframe.contentDocument || iframe.contentWindow?.document;
210
+ if (iframeContent) {
211
+ injectScriptInIframe(iframeContent);
212
+ }
213
+ }
214
+ }
215
+ });
185
216
  }
186
217
  catch (e) {
187
- // skip errors which can occur while the page is getting stable
218
+ console.warn("Error during script injection on page load:", e);
188
219
  }
189
220
  });
190
- await Promise.all(scripts.map((s) => page.addScriptTag({ content: s })));
221
+ try {
222
+ await Promise.all(scripts.map((s) => page.addScriptTag({ content: s })));
223
+ await page.evaluate(async () => {
224
+ //@ts-ignore
225
+ const injectScriptInIframe = (iframeDoc) => {
226
+ try {
227
+ [
228
+ "https://assets-test.empirical.run/pw-selector.js",
229
+ "https://code.jquery.com/jquery-3.7.1.min.js",
230
+ ].forEach((url) => {
231
+ const scr = iframeDoc.createElement("script");
232
+ scr.src = url;
233
+ console.log("Injecting script in iframe", scr);
234
+ iframeDoc.head.appendChild(scr);
235
+ });
236
+ }
237
+ catch (e) {
238
+ console.warn("Error injecting script in iframe:", e);
239
+ }
240
+ };
241
+ const iframes = document.getElementsByTagName("iframe");
242
+ for (const iframe of iframes) {
243
+ iframe.getBoundingClientRect();
244
+ const rect = iframe.getBoundingClientRect();
245
+ const isVisible = rect.width > 0 && rect.height > 0;
246
+ if (isVisible) {
247
+ //@ts-ignore
248
+ const iframeContent = iframe.contentDocument || iframe.contentWindow?.document;
249
+ if (iframeContent) {
250
+ injectScriptInIframe(iframeContent);
251
+ }
252
+ }
253
+ }
254
+ });
255
+ }
256
+ catch (e) {
257
+ console.warn("Error injecting script in iframe:", e);
258
+ }
191
259
  }
192
260
  exports.injectPwLocatorGenerator = injectPwLocatorGenerator;
193
261
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/agent/master/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,GAAG,EACH,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAclD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EACL,oBAAoB,EAErB,MAAM,aAAa,CAAC;AAoBrB,wBAAsB,aAAa,CAAC,EAClC,IAAI,EACJ,eAAe,EACf,aAAa,EACb,OAAO,EACP,KAAK,EACL,GAAG,EACH,OAAO,EACP,cAAc,EACd,uBAAuB,EACvB,OAAO,EACP,aAAa,EACb,QAAgB,EAChB,WAAW,GACZ,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,GAAG,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,GAAG,CAAC,EAAE,GAAG,CAAC;IACV,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,OAAO,EAAE,iBAAiB,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,2FA2FA;AAGD,wBAAsB,0BAA0B,CAAC,EAC/C,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,SAAS,GACV,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,oBAAoB,CAAC;IAC9B,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;;;GAiUA"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/agent/master/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,GAAG,EACH,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAclD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EACL,oBAAoB,EAErB,MAAM,aAAa,CAAC;AAoBrB,wBAAsB,aAAa,CAAC,EAClC,IAAI,EACJ,eAAe,EACf,aAAa,EACb,OAAO,EACP,KAAK,EACL,GAAG,EACH,OAAO,EACP,cAAc,EACd,uBAAuB,EACvB,OAAO,EACP,aAAa,EACb,QAAgB,EAChB,WAAW,GACZ,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,GAAG,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,GAAG,CAAC,EAAE,GAAG,CAAC;IACV,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,OAAO,EAAE,iBAAiB,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,2FA2FA;AAGD,wBAAsB,0BAA0B,CAAC,EAC/C,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,SAAS,GACV,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,oBAAoB,CAAC;IAC9B,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;;;GAsTA"}
@@ -202,17 +202,17 @@ async function createTestUsingMasterAgent({ task, page, testCase, options, scope
202
202
  const pageScreenshot = buffer.toString("base64");
203
203
  let output;
204
204
  let generatedCodeSteps = [];
205
- let annotations;
206
205
  let annotatedPageScreenshot;
206
+ let annotationKeys = [];
207
207
  if (useHints) {
208
208
  await page.waitForTimeout(2000);
209
- const annotationResult = await page.evaluate(() => {
209
+ annotationKeys = await page.evaluate(() => {
210
210
  // @ts-ignore
211
- window.annotationInstance = window.annotateClickableElements();
211
+ // eslint-disable-next-line no-undef
212
+ window.annotationInstance = annotateClickableElements();
212
213
  // @ts-ignore
213
- return window.annotationInstance;
214
+ return Object.keys(window.annotationInstance.annotations);
214
215
  });
215
- annotations = annotationResult?.annotations || {};
216
216
  await page.waitForTimeout(2000);
217
217
  const annonationBuffer = await page.screenshot({
218
218
  //This is done to improve element annotation accuracy, anyways it doesn't annotate elements which are out of viewport
@@ -236,7 +236,6 @@ async function createTestUsingMasterAgent({ task, page, testCase, options, scope
236
236
  if (await (0, session_1.shouldStopSession)()) {
237
237
  break;
238
238
  }
239
- const annotationKeys = annotations ? Object.keys(annotations) : [];
240
239
  const toolCall = await getNextAction({
241
240
  task,
242
241
  executedActions: masterAgentActions,
@@ -275,12 +274,12 @@ async function createTestUsingMasterAgent({ task, page, testCase, options, scope
275
274
  name: "trigger-hints-flow",
276
275
  input: {
277
276
  outputFromGetNextAction: output,
278
- generatedAnnotations: annotations,
277
+ generatedAnnotations: annotationKeys,
279
278
  },
280
279
  });
281
280
  const result = await (0, with_hints_1.triggerHintsFlow)({
282
281
  outputFromGetNextAction: output,
283
- generatedAnnotations: annotations,
282
+ generatedAnnotations: annotationKeys,
284
283
  page: testGenPage,
285
284
  llm,
286
285
  trace: triggerHintsFlowSpan,
@@ -1 +1 @@
1
- {"version":3,"file":"with-hints.d.ts","sourceRoot":"","sources":["../../../src/agent/master/with-hints.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,MAAM,MAAM,QAAQ,CAAC;AAI5B,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,eAAO,MAAM,0BAA0B;iBAMxB,OAAO,8BAA8B;;oBAElC,MAAM;6BACG,MAAM;MAC7B,MAAM,GAAG,OAAO,yBAAyB,EAiC5C,CAAC;AAEF,eAAO,MAAM,gBAAgB;6BAOF;QACvB,MAAM,EAAE,MAAM,CAAC;QACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC5B;0BACqB,OAAO,MAAM,EAAE,GAAG,CAAC;UACnC,WAAW;SACZ,GAAG;;MAEN,QAAQ;IACV,sBAAsB,EAAE,OAAO,CAAC;IAChC,wBAAwB,EAAE,OAAO,qBAAqB,GAAG,SAAS,CAAC;CACpE,CAqGA,CAAC"}
1
+ {"version":3,"file":"with-hints.d.ts","sourceRoot":"","sources":["../../../src/agent/master/with-hints.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,MAAM,MAAM,QAAQ,CAAC;AAI5B,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,eAAO,MAAM,0BAA0B;iBAMxB,OAAO,8BAA8B;;oBAElC,MAAM;6BACG,MAAM;MAC7B,MAAM,GAAG,OAAO,yBAAyB,EAiC5C,CAAC;AAEF,eAAO,MAAM,gBAAgB;6BAOF;QACvB,MAAM,EAAE,MAAM,CAAC;QACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC5B;0BACqB,OAAO,MAAM,EAAE,GAAG,CAAC;UACnC,WAAW;SACZ,GAAG;;MAEN,QAAQ;IACV,sBAAsB,EAAE,OAAO,CAAC;IAChC,wBAAwB,EAAE,OAAO,qBAAqB,GAAG,SAAS,CAAC;CACpE,CAuGA,CAAC"}
@@ -37,12 +37,12 @@ const triggerHintsFlow = async ({ outputFromGetNextAction, generatedAnnotations,
37
37
  try {
38
38
  const hasElementAnnotation = outputFromGetNextAction?.elementAnnotation?.length &&
39
39
  outputFromGetNextAction?.elementAnnotation?.trim()?.length &&
40
- outputFromGetNextAction?.elementAnnotation in
41
- (generatedAnnotations || {});
40
+ generatedAnnotations?.includes(outputFromGetNextAction?.elementAnnotation);
42
41
  trace?.event({
43
42
  name: "has-element-annotation",
44
43
  output: {
45
44
  hasElementAnnotation,
45
+ generatedAnnotations,
46
46
  },
47
47
  });
48
48
  if (!hasElementAnnotation) {
@@ -9,28 +9,20 @@
9
9
  * @param {string} options.markerClass - CSS class to apply to hint markers.
10
10
  * @returns {Object} An object containing annotations map and enable/disable methods.
11
11
  */
12
- window.annotateClickableElements = function annotateClickableElements(
13
- options = {},
14
- ) {
12
+ function annotateClickableElements(options = {}) {
15
13
  const {
16
14
  hintCharacterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", // Default set of characters for hints
17
15
  maxHints = 1000, // Maximum number of hints to generate
18
16
  markerClass = "hint-marker", // CSS class for markers
19
17
  } = options;
20
18
 
19
+ const document = window.document;
21
20
  const annotationsMap = {};
21
+ const usedHints = new Set();
22
22
  let annotationsContainer = null;
23
23
 
24
- /**
25
- * Checks if the provided DOM element is clickable by ensuring that
26
- * no other elements are covering it, regardless of its current
27
- * viewport visibility.
28
- *
29
- * @param {Element} element - The DOM element to check.
30
- * @returns {boolean} - Returns true if the element is clickable; otherwise, false.
31
- */
32
-
33
- function isElementClickNotBlocked(element) {
24
+ // Check if the element is not blocked and visible for clicking
25
+ function isElementClickNotBlocked(element, windowToAnnotate) {
34
26
  const rect = element.getBoundingClientRect();
35
27
 
36
28
  // Calculate the center point of the element
@@ -41,21 +33,21 @@ window.annotateClickableElements = function annotateClickableElements(
41
33
  if (
42
34
  centerX < 0 ||
43
35
  centerY < 0 ||
44
- centerX > (window.innerWidth || document.documentElement.clientWidth) ||
45
- centerY > (window.innerHeight || document.documentElement.clientHeight)
36
+ centerX > (windowToAnnotate.innerWidth || document.documentElement.clientWidth) ||
37
+ centerY > (windowToAnnotate.innerHeight || document.documentElement.clientHeight)
46
38
  ) {
47
39
  // Determine the viewport dimensions
48
40
  const viewportWidth =
49
- window.innerWidth || document.documentElement.clientWidth;
41
+ windowToAnnotate.innerWidth || document.documentElement.clientWidth;
50
42
  const viewportHeight =
51
- window.innerHeight || document.documentElement.clientHeight;
43
+ windowToAnnotate.innerHeight || document.documentElement.clientHeight;
52
44
 
53
45
  // Calculate the new scroll positions to bring the element into the center of the viewport
54
46
  const newScrollX = centerX - viewportWidth / 2;
55
47
  const newScrollY = centerY - viewportHeight / 2;
56
48
 
57
49
  // Scroll the window to the new positions
58
- window.scrollTo({
50
+ windowToAnnotate.scrollTo({
59
51
  top: newScrollY,
60
52
  left: newScrollX,
61
53
  });
@@ -67,20 +59,12 @@ window.annotateClickableElements = function annotateClickableElements(
67
59
  return element.contains(topElement);
68
60
  }
69
61
 
70
- // Retrieve the topmost element at the center point
71
- const topElement = document.elementFromPoint(centerX, centerY);
62
+ const topElement = windowToAnnotate.document.elementFromPoint(centerX, centerY);
72
63
 
73
64
  // Check if the topmost element is the target element or one of its descendants
74
65
  return element.contains(topElement);
75
66
  }
76
67
 
77
- /**
78
- * Generates a sequence of hint strings based on the provided character set.
79
- * For example, with 'abc' as the set and maxHints 5, it generates: 'a', 'b', 'c', 'aa', 'ab'
80
- * @param {string} charset - Characters to use.
81
- * @param {number} max - Maximum number of hints to generate.
82
- * @returns {string[]} Array of hint strings.
83
- */
84
68
  function generateHintStrings(charset, max) {
85
69
  const hints = [];
86
70
  let length = 1;
@@ -88,7 +72,11 @@ window.annotateClickableElements = function annotateClickableElements(
88
72
  while (hints.length < max) {
89
73
  const combos = cartesianProduct(Array(length).fill(charset.split("")));
90
74
  for (const combo of combos) {
91
- hints.push(combo.join(""));
75
+ const hint = combo.join("");
76
+ if (!usedHints.has(hint)) {
77
+ hints.push(hint);
78
+ usedHints.add(hint);
79
+ }
92
80
  if (hints.length >= max) break;
93
81
  }
94
82
  length++;
@@ -97,29 +85,19 @@ window.annotateClickableElements = function annotateClickableElements(
97
85
  return hints;
98
86
  }
99
87
 
100
- /**
101
- * Creates a Cartesian product of arrays.
102
- * @param {Array<Array>} arrays - Array of arrays for Cartesian product.
103
- * @returns {Array<Array>} Cartesian product.
104
- */
105
88
  function cartesianProduct(arrays) {
106
89
  return arrays.reduce(
107
90
  (acc, curr) =>
108
91
  acc
109
92
  .map((a) => curr.map((b) => a.concat([b])))
110
93
  .reduce((a, b) => a.concat(b), []),
111
- [[]],
94
+ [[]]
112
95
  );
113
96
  }
114
97
 
115
- /**
116
- * Checks if an element is visible and interactable.
117
- * Enhanced version inspired by getLocalHintsForElement.
118
- * @param {Element} element - DOM element.
119
- * @returns {boolean} True if the element is clickable and visible.
120
- */
98
+ // Check if an element is clickable
121
99
  function isElementClickable(element) {
122
- if (!(element instanceof Element)) return false;
100
+ // if (!(element instanceof Element)) return false;
123
101
 
124
102
  const tagName = element.tagName.toLowerCase();
125
103
  let isClickable = false;
@@ -311,11 +289,6 @@ window.annotateClickableElements = function annotateClickableElements(
311
289
  return isClickable;
312
290
  }
313
291
 
314
- /**
315
- * Helper function to determine if an input is selectable (e.g., text, search).
316
- * @param {Element} input - The input element.
317
- * @returns {boolean} True if the input type is selectable.
318
- */
319
292
  function isInputSelectable(input) {
320
293
  const selectableTypes = [
321
294
  "text",
@@ -330,68 +303,23 @@ window.annotateClickableElements = function annotateClickableElements(
330
303
  return selectableTypes.includes(type);
331
304
  }
332
305
 
333
- /**
334
- * Generates a unique CSS selector path for a given element.
335
- * @param {Element} element - The DOM element.
336
- * @returns {string} A unique CSS selector string.
337
- */
338
- function getUniqueSelector(element) {
339
- if (element.id) {
340
- return `#${CSS.escape(element.id)}`;
341
- }
342
-
343
- const parts = [];
344
- while (element && element.nodeType === Node.ELEMENT_NODE) {
345
- let part = element.nodeName.toLowerCase();
346
-
347
- // Use classList instead of className to avoid errors
348
- if (element.classList && element.classList.length > 0) {
349
- const classes = Array.from(element.classList)
350
- .filter((cls) => cls.length > 0)
351
- .map((cls) => `.${CSS.escape(cls)}`)
352
- .join("");
353
- part += classes;
354
- }
355
-
356
- // Add nth-child if necessary
357
- const parent = element.parentNode;
358
- if (parent) {
359
- const siblings = Array.from(parent.children).filter(
360
- (sibling) => sibling.nodeName === element.nodeName,
361
- );
362
- if (siblings.length > 1) {
363
- const index =
364
- Array.prototype.indexOf.call(parent.children, element) + 1;
365
- part += `:nth-child(${index})`;
366
- }
367
- }
368
-
369
- parts.unshift(part);
370
- element = element.parentElement;
371
- }
372
-
373
- return parts.join(" > ");
374
- }
306
+ var parentElements = [];
375
307
 
376
- /**
377
- * Creates a hint marker element positioned at the top-left of the target element.
378
- * @param {Element} el - The clickable DOM element.
379
- * @param {string} hint - The hint identifier.
380
- * @param {string} nodePath - Unique CSS selector path for the element.
381
- * @returns {HTMLElement} The created hint marker element.
382
- */
383
- function createHintMarker(el, hint) {
308
+ // Create a hint marker
309
+ function createHintMarker(el, hint, parentElement) {
384
310
  const rect = el.getBoundingClientRect();
385
-
311
+ const parentRect = parentElement.getBoundingClientRect();
312
+
313
+ // Create the marker element
386
314
  const marker = document.createElement("div");
387
315
  marker.textContent = hint;
388
316
  marker.className = markerClass;
389
-
317
+
390
318
  // Style the marker
391
319
  Object.assign(marker.style, {
392
320
  position: "absolute",
393
- top: `${rect.top + window.scrollY}px`,
394
- left: `${rect.left + window.scrollX}px`,
321
+ top: `${rect.top + parentRect.top}px`,
322
+ left: `${rect.left + parentRect.left}px`,
395
323
  background:
396
324
  "-webkit-gradient(linear, 0% 0%, 0% 100%, from(rgb(255, 247, 133)), to(rgb(255, 197, 66)))",
397
325
  padding: "1px 3px 0px",
@@ -411,116 +339,116 @@ window.annotateClickableElements = function annotateClickableElements(
411
339
  fontWeight: "bold",
412
340
  textShadow: "rgba(255, 255, 255, 0.6) 0px 1px 0px",
413
341
  });
414
-
415
- annotationsContainer.appendChild(marker);
416
-
342
+
343
+ // Attach the marker to the specified parent element
344
+ parentElement.appendChild(marker);
345
+ parentElements.push(parentElement);
417
346
  return marker;
418
347
  }
348
+
419
349
 
420
- /**
421
- * Clears all existing hint markers from the annotations container.
422
- */
350
+ // Clear existing annotations
351
+ //TODO: Handle clearing annotations
423
352
  function clearAnnotations() {
424
- if (annotationsContainer) {
425
- annotationsContainer.remove();
426
- annotationsContainer = null;
427
- }
428
- }
429
-
430
- /**
431
- * Initializes the annotations by creating the container and adding markers.
432
- */
433
- function initializeAnnotations() {
434
- clearAnnotations(); // Clear any existing annotations
435
-
436
- annotationsContainer = document.createElement("div");
437
- annotationsContainer.className = "annotations";
438
- // Ensure the container covers the entire page
439
- Object.assign(annotationsContainer.style, {
440
- position: "absolute",
441
- top: "0",
442
- left: "0",
443
- width: "100%",
444
- height: "100%",
445
- pointerEvents: "none", // Allow clicks to pass through
446
- zIndex: "9999", // Ensure it's above other elements
353
+ parentElements.forEach((parentElement) => {
354
+ const markers = parentElement.querySelectorAll(`.${markerClass}`);
355
+ markers.forEach((marker) => marker.remove());
447
356
  });
448
- document.body.appendChild(annotationsContainer);
449
-
450
- const clickableElements = Array.from(document.querySelectorAll("*")).filter(
451
- (el) => {
452
- const isClickable = isElementClickable(el);
453
-
454
- const originalScrollX = window.scrollX;
455
- const originalScrollY = window.scrollY;
456
-
457
- const isClickNotBlocked = isElementClickNotBlocked(el);
458
- // Restore the original scroll positions
459
- window.scrollTo(originalScrollX, originalScrollY);
460
-
461
- return isClickable && isClickNotBlocked;
462
- },
463
- );
464
- const hints = generateHintStrings(
465
- hintCharacterSet,
466
- Math.min(maxHints, clickableElements.length),
467
- );
357
+ parentElements = [];
358
+ }
468
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
469
378
  clickableElements.slice(0, maxHints).forEach((el, index) => {
470
379
  const hint = hints[index];
471
- const nodePath = getUniqueSelector(el);
472
380
  const rect = el.getBoundingClientRect();
473
-
474
- // Create the hint marker
475
- createHintMarker(el, hint, nodePath);
381
+
382
+ // Use createHintMarker with the specified container
383
+ createHintMarker(el, hint, container);
476
384
  el.style.boxShadow = `inset 0 0 0px 2px red`;
477
385
 
478
- // Populate the annotations map
386
+ // Add element details to the annotations map
479
387
  annotationsMap[hint] = {
480
388
  node: el,
481
- nodePath: nodePath,
482
389
  rect: {
483
- top: rect.top + window.scrollY,
484
- left: rect.left + window.scrollX,
390
+ top: rect.top + windowToAnnotate.scrollY,
391
+ left: rect.left + windowToAnnotate.scrollX,
485
392
  width: rect.width,
486
393
  height: rect.height,
487
- right: rect.right + window.scrollX,
488
- bottom: rect.bottom + window.scrollY,
489
394
  },
395
+ depth: [...depth ]
490
396
  };
491
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
+ });
492
410
  }
411
+
493
412
 
494
- /**
495
- * Enables the annotations by making the annotations container visible.
496
- */
413
+ // Initialize and enable annotations
497
414
  function enable() {
415
+ clearAnnotations();
498
416
  if (!annotationsContainer) {
499
- initializeAnnotations();
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, []);
500
430
  } else {
501
431
  annotationsContainer.style.display = "block";
502
432
  }
503
433
  }
504
434
 
505
- /**
506
- * removes all generated hints from DOM
507
- */
435
+ // Destroy annotations
508
436
  function destroy() {
509
437
  if (annotationsContainer) {
438
+ clearAnnotations();
510
439
  Object.values(annotationsMap).forEach((annotation) => {
511
440
  annotation.node.style.boxShadow = "none";
512
441
  });
513
442
 
514
443
  annotationsContainer.parentNode.removeChild(annotationsContainer);
444
+ annotationsContainer = null;
515
445
  }
516
446
  }
517
447
 
518
- // Initialize annotations upon first run
519
448
  enable();
520
449
 
521
- // Return the desired object
522
450
  return {
523
451
  annotations: annotationsMap,
524
452
  destroy,
525
453
  };
526
- };
454
+ }
@@ -15,7 +15,7 @@ test("should annotate all links on empirical landing page", async ({
15
15
  });
16
16
 
17
17
  const annotations = await page.evaluate(() => {
18
- const { annotations } = window.annotateClickableElements();
18
+ const { annotations } = annotateClickableElements();
19
19
 
20
20
  return Object.entries(annotations).map(([hint, config]) => ({
21
21
  innerText: config.node.innerText,
@@ -80,7 +80,7 @@ test("should annotate all important items on quizizz page", async ({
80
80
  });
81
81
 
82
82
  const annotations = await page.evaluate(() => {
83
- const { annotations } = window.annotateClickableElements();
83
+ const { annotations } = annotateClickableElements();
84
84
 
85
85
  return Object.entries(annotations).map(([hint, config]) => ({
86
86
  hint,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/test-gen",
3
- "version": "0.38.5",
3
+ "version": "0.38.7",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"