@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 +12 -0
- package/browser-injected-scripts/annotate-elements.js +97 -169
- package/browser-injected-scripts/annotate-elements.spec.ts +2 -2
- package/dist/actions/utils/index.d.ts.map +1 -1
- package/dist/actions/utils/index.js +25 -0
- package/dist/agent/browsing/utils.d.ts.map +1 -1
- package/dist/agent/browsing/utils.js +71 -3
- package/dist/agent/master/run.d.ts.map +1 -1
- package/dist/agent/master/run.js +7 -8
- package/dist/agent/master/with-hints.d.ts.map +1 -1
- package/dist/agent/master/with-hints.js +2 -2
- package/dist/browser-injected-scripts/annotate-elements.js +97 -169
- package/dist/browser-injected-scripts/annotate-elements.spec.ts +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 > (
|
|
45
|
-
centerY > (
|
|
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
|
-
|
|
41
|
+
windowToAnnotate.innerWidth || document.documentElement.clientWidth;
|
|
50
42
|
const viewportHeight =
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 +
|
|
394
|
-
left: `${rect.left +
|
|
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
|
-
|
|
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
|
-
|
|
422
|
-
*/
|
|
350
|
+
// Clear existing annotations
|
|
351
|
+
//TODO: Handle clearing annotations
|
|
423
352
|
function clearAnnotations() {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
475
|
-
createHintMarker(el, hint,
|
|
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
|
-
//
|
|
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 +
|
|
484
|
-
left: rect.left +
|
|
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
|
-
|
|
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 } =
|
|
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 } =
|
|
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,
|
|
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,
|
|
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
|
-
|
|
218
|
+
console.warn("Error during script injection on page load:", e);
|
|
188
219
|
}
|
|
189
220
|
});
|
|
190
|
-
|
|
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;;;
|
|
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"}
|
package/dist/agent/master/run.js
CHANGED
|
@@ -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
|
-
|
|
209
|
+
annotationKeys = await page.evaluate(() => {
|
|
210
210
|
// @ts-ignore
|
|
211
|
-
|
|
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:
|
|
277
|
+
generatedAnnotations: annotationKeys,
|
|
279
278
|
},
|
|
280
279
|
});
|
|
281
280
|
const result = await (0, with_hints_1.triggerHintsFlow)({
|
|
282
281
|
outputFromGetNextAction: output,
|
|
283
|
-
generatedAnnotations:
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 > (
|
|
45
|
-
centerY > (
|
|
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
|
-
|
|
41
|
+
windowToAnnotate.innerWidth || document.documentElement.clientWidth;
|
|
50
42
|
const viewportHeight =
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 +
|
|
394
|
-
left: `${rect.left +
|
|
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
|
-
|
|
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
|
-
|
|
422
|
-
*/
|
|
350
|
+
// Clear existing annotations
|
|
351
|
+
//TODO: Handle clearing annotations
|
|
423
352
|
function clearAnnotations() {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
475
|
-
createHintMarker(el, hint,
|
|
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
|
-
//
|
|
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 +
|
|
484
|
-
left: rect.left +
|
|
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
|
-
|
|
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 } =
|
|
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 } =
|
|
83
|
+
const { annotations } = annotateClickableElements();
|
|
84
84
|
|
|
85
85
|
return Object.entries(annotations).map(([hint, config]) => ({
|
|
86
86
|
hint,
|