@empiricalrun/test-gen 0.35.1 → 0.35.4
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 +19 -0
- package/browser-injected-scripts/annotate-elements.js +522 -0
- package/dist/actions/index.d.ts +8 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +13 -1
- package/dist/agent/browsing/index.d.ts.map +1 -1
- package/dist/agent/browsing/index.js +2 -4
- package/dist/agent/browsing/utils.d.ts.map +1 -1
- package/dist/agent/browsing/utils.js +11 -3
- package/dist/agent/master/run.d.ts.map +1 -1
- package/dist/agent/master/run.js +9 -4
- package/dist/agent/master/with-hints.d.ts.map +1 -1
- package/dist/agent/master/with-hints.js +1 -4
- package/dist/bin/index.js +9 -0
- package/dist/browser-injected-scripts/annotate-elements.js +522 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/session/index.d.ts +4 -1
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +4 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @empiricalrun/test-gen
|
|
2
2
|
|
|
3
|
+
## 0.35.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4bddc82: fix: annotate all non-blocked elements, outside the viewport too
|
|
8
|
+
|
|
9
|
+
## 0.35.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 19f5344: fix: added test url in trace metadata
|
|
14
|
+
|
|
15
|
+
## 0.35.2
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 2821db3: fix: handle agent getting stuck in loop
|
|
20
|
+
- 0fe1f6e: fix: load annotate-elements script from disk
|
|
21
|
+
|
|
3
22
|
## 0.35.1
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
/* eslint-disable no-undef */
|
|
2
|
+
/**
|
|
3
|
+
* Annotates all clickable elements on the page with unique hint markers.
|
|
4
|
+
* Returns an object containing annotations and methods to enable/disable them.
|
|
5
|
+
*
|
|
6
|
+
* @param {Object} options - Configuration options for hint markers.
|
|
7
|
+
* @param {string} options.hintCharacterSet - Characters to use for generating hint identifiers.
|
|
8
|
+
* @param {number} options.maxHints - Maximum number of hints to generate.
|
|
9
|
+
* @param {string} options.markerClass - CSS class to apply to hint markers.
|
|
10
|
+
* @returns {Object} An object containing annotations map and enable/disable methods.
|
|
11
|
+
*/
|
|
12
|
+
window.annotateClickableElements = function annotateClickableElements(
|
|
13
|
+
options = {},
|
|
14
|
+
) {
|
|
15
|
+
const {
|
|
16
|
+
hintCharacterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", // Default set of characters for hints
|
|
17
|
+
maxHints = 1000, // Maximum number of hints to generate
|
|
18
|
+
markerClass = "hint-marker", // CSS class for markers
|
|
19
|
+
} = options;
|
|
20
|
+
|
|
21
|
+
const annotationsMap = {};
|
|
22
|
+
let annotationsContainer = null;
|
|
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) {
|
|
34
|
+
const rect = element.getBoundingClientRect();
|
|
35
|
+
|
|
36
|
+
// Calculate the center point of the element
|
|
37
|
+
const centerX = rect.left + rect.width / 2;
|
|
38
|
+
const centerY = rect.top + rect.height / 2;
|
|
39
|
+
|
|
40
|
+
// check if element is within the viewport
|
|
41
|
+
if (
|
|
42
|
+
centerX < 0 ||
|
|
43
|
+
centerY < 0 ||
|
|
44
|
+
centerX > (window.innerWidth || document.documentElement.clientWidth) ||
|
|
45
|
+
centerY > (window.innerHeight || document.documentElement.clientHeight)
|
|
46
|
+
) {
|
|
47
|
+
// Determine the viewport dimensions
|
|
48
|
+
const viewportWidth =
|
|
49
|
+
window.innerWidth || document.documentElement.clientWidth;
|
|
50
|
+
const viewportHeight =
|
|
51
|
+
window.innerHeight || document.documentElement.clientHeight;
|
|
52
|
+
|
|
53
|
+
// Calculate the new scroll positions to bring the element into the center of the viewport
|
|
54
|
+
const newScrollX = centerX - viewportWidth / 2;
|
|
55
|
+
const newScrollY = centerY - viewportHeight / 2;
|
|
56
|
+
|
|
57
|
+
// Scroll the window to the new positions
|
|
58
|
+
window.scrollTo({
|
|
59
|
+
top: newScrollY,
|
|
60
|
+
left: newScrollX,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const newRect = element.getBoundingClientRect();
|
|
64
|
+
const newCenterX = newRect.left + newRect.width / 2;
|
|
65
|
+
const newCenterY = newRect.top + newRect.height / 2;
|
|
66
|
+
const topElement = document.elementFromPoint(newCenterX, newCenterY);
|
|
67
|
+
return element.contains(topElement);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Retrieve the topmost element at the center point
|
|
71
|
+
const topElement = document.elementFromPoint(centerX, centerY);
|
|
72
|
+
|
|
73
|
+
// Check if the topmost element is the target element or one of its descendants
|
|
74
|
+
return element.contains(topElement);
|
|
75
|
+
}
|
|
76
|
+
|
|
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
|
+
function generateHintStrings(charset, max) {
|
|
85
|
+
const hints = [];
|
|
86
|
+
let length = 1;
|
|
87
|
+
|
|
88
|
+
while (hints.length < max) {
|
|
89
|
+
const combos = cartesianProduct(Array(length).fill(charset.split("")));
|
|
90
|
+
for (const combo of combos) {
|
|
91
|
+
hints.push(combo.join(""));
|
|
92
|
+
if (hints.length >= max) break;
|
|
93
|
+
}
|
|
94
|
+
length++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return hints;
|
|
98
|
+
}
|
|
99
|
+
|
|
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
|
+
function cartesianProduct(arrays) {
|
|
106
|
+
return arrays.reduce(
|
|
107
|
+
(acc, curr) =>
|
|
108
|
+
acc
|
|
109
|
+
.map((a) => curr.map((b) => a.concat([b])))
|
|
110
|
+
.reduce((a, b) => a.concat(b), []),
|
|
111
|
+
[[]],
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
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
|
+
*/
|
|
121
|
+
function isElementClickable(element) {
|
|
122
|
+
if (!(element instanceof Element)) return false;
|
|
123
|
+
|
|
124
|
+
const tagName = element.tagName.toLowerCase();
|
|
125
|
+
let isClickable = false;
|
|
126
|
+
|
|
127
|
+
// Check for aria-disabled
|
|
128
|
+
const ariaDisabled = element.getAttribute("aria-disabled");
|
|
129
|
+
if (ariaDisabled && ["", "true"].includes(ariaDisabled.toLowerCase())) {
|
|
130
|
+
return false; // Element should not be clickable if aria-disabled is true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check for visibility
|
|
134
|
+
const style = window.getComputedStyle(element);
|
|
135
|
+
if (
|
|
136
|
+
style.display === "none" ||
|
|
137
|
+
style.visibility === "hidden" ||
|
|
138
|
+
parseFloat(style.opacity) === 0 ||
|
|
139
|
+
style.pointerEvents === "none"
|
|
140
|
+
) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check if element is disabled (for applicable elements)
|
|
145
|
+
if (element.disabled) return false;
|
|
146
|
+
|
|
147
|
+
// Check for AngularJS click handlers
|
|
148
|
+
if (!isElementClickable._checkForAngularJs) {
|
|
149
|
+
isElementClickable._checkForAngularJs = (function () {
|
|
150
|
+
const angularElements = document.getElementsByClassName("ng-scope");
|
|
151
|
+
if (angularElements.length === 0) {
|
|
152
|
+
return () => false;
|
|
153
|
+
} else {
|
|
154
|
+
const ngAttributes = [];
|
|
155
|
+
for (const prefix of ["", "data-", "x-"]) {
|
|
156
|
+
for (const separator of ["-", ":", "_"]) {
|
|
157
|
+
ngAttributes.push(`${prefix}ng${separator}click`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return function (el) {
|
|
161
|
+
for (const attribute of ngAttributes) {
|
|
162
|
+
if (el.hasAttribute(attribute)) return true;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
})();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!isClickable && isElementClickable._checkForAngularJs(element)) {
|
|
171
|
+
isClickable = true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check for onclick attribute or listener
|
|
175
|
+
if (
|
|
176
|
+
element.hasAttribute("onclick") ||
|
|
177
|
+
typeof element.onclick === "function"
|
|
178
|
+
) {
|
|
179
|
+
isClickable = true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check for jsaction attribute (commonly used in frameworks like Google's)
|
|
183
|
+
if (!isClickable && element.hasAttribute("jsaction")) {
|
|
184
|
+
const jsactionRules = element.getAttribute("jsaction").split(";");
|
|
185
|
+
for (const jsactionRule of jsactionRules) {
|
|
186
|
+
const ruleSplit = jsactionRule.trim().split(":");
|
|
187
|
+
if (ruleSplit.length >= 1 && ruleSplit.length <= 2) {
|
|
188
|
+
const [eventType] = ruleSplit[0].trim().split(".");
|
|
189
|
+
if (eventType === "click") {
|
|
190
|
+
isClickable = true;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check for role attributes that imply clickability
|
|
198
|
+
if (!isClickable) {
|
|
199
|
+
const role = element.getAttribute("role");
|
|
200
|
+
const clickableRoles = [
|
|
201
|
+
"button",
|
|
202
|
+
"tab",
|
|
203
|
+
"link",
|
|
204
|
+
"checkbox",
|
|
205
|
+
"menuitem",
|
|
206
|
+
"menuitemcheckbox",
|
|
207
|
+
"menuitemradio",
|
|
208
|
+
"radio",
|
|
209
|
+
"switch",
|
|
210
|
+
];
|
|
211
|
+
if (role && clickableRoles.includes(role.toLowerCase())) {
|
|
212
|
+
isClickable = true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check for contentEditable
|
|
217
|
+
if (!isClickable) {
|
|
218
|
+
const contentEditable = element.getAttribute("contentEditable");
|
|
219
|
+
if (
|
|
220
|
+
contentEditable != null &&
|
|
221
|
+
["", "contenteditable", "true"].includes(contentEditable.toLowerCase())
|
|
222
|
+
) {
|
|
223
|
+
isClickable = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Tag-specific clickability
|
|
228
|
+
const focusableTags = [
|
|
229
|
+
"a",
|
|
230
|
+
"button",
|
|
231
|
+
"input",
|
|
232
|
+
"select",
|
|
233
|
+
"textarea",
|
|
234
|
+
"object",
|
|
235
|
+
"embed",
|
|
236
|
+
"label",
|
|
237
|
+
"details",
|
|
238
|
+
];
|
|
239
|
+
if (focusableTags.includes(tagName)) {
|
|
240
|
+
switch (tagName) {
|
|
241
|
+
case "a":
|
|
242
|
+
// Ensure it's not just a named anchor without href
|
|
243
|
+
if (element.hasAttribute("href")) {
|
|
244
|
+
isClickable = true;
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
case "input": {
|
|
248
|
+
const type = (element.getAttribute("type") || "").toLowerCase();
|
|
249
|
+
if (
|
|
250
|
+
type !== "hidden" &&
|
|
251
|
+
!element.disabled &&
|
|
252
|
+
!(element.readOnly && isInputSelectable(element))
|
|
253
|
+
) {
|
|
254
|
+
isClickable = true;
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case "textarea":
|
|
259
|
+
if (!element.disabled && !element.readOnly) {
|
|
260
|
+
isClickable = true;
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
case "button":
|
|
264
|
+
case "select":
|
|
265
|
+
if (!element.disabled) {
|
|
266
|
+
isClickable = true;
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
case "object":
|
|
270
|
+
case "embed":
|
|
271
|
+
isClickable = true;
|
|
272
|
+
break;
|
|
273
|
+
case "label":
|
|
274
|
+
if (element.control && !element.control.disabled) {
|
|
275
|
+
isClickable = true;
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
case "details":
|
|
279
|
+
isClickable = true;
|
|
280
|
+
break;
|
|
281
|
+
default:
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check for class names containing 'button' as a possible click target
|
|
287
|
+
if (!isClickable) {
|
|
288
|
+
const className = element.getAttribute("class");
|
|
289
|
+
if (className && className.toLowerCase().includes("button")) {
|
|
290
|
+
isClickable = true;
|
|
291
|
+
} else if (element.classList.contains("cursor-pointer")) {
|
|
292
|
+
isClickable = true;
|
|
293
|
+
} else if (element.style.cursor === "pointer") {
|
|
294
|
+
isClickable = true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check for tabindex
|
|
299
|
+
if (!isClickable) {
|
|
300
|
+
const tabIndexValue = element.getAttribute("tabindex");
|
|
301
|
+
const tabIndex = tabIndexValue ? parseInt(tabIndexValue) : -1;
|
|
302
|
+
if (tabIndex >= 0 && !isNaN(tabIndex)) {
|
|
303
|
+
isClickable = true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return isClickable;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Helper function to determine if an input is selectable (e.g., text, search).
|
|
312
|
+
* @param {Element} input - The input element.
|
|
313
|
+
* @returns {boolean} True if the input type is selectable.
|
|
314
|
+
*/
|
|
315
|
+
function isInputSelectable(input) {
|
|
316
|
+
const selectableTypes = [
|
|
317
|
+
"text",
|
|
318
|
+
"search",
|
|
319
|
+
"password",
|
|
320
|
+
"url",
|
|
321
|
+
"email",
|
|
322
|
+
"number",
|
|
323
|
+
"tel",
|
|
324
|
+
];
|
|
325
|
+
const type = (input.getAttribute("type") || "").toLowerCase();
|
|
326
|
+
return selectableTypes.includes(type);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Generates a unique CSS selector path for a given element.
|
|
331
|
+
* @param {Element} element - The DOM element.
|
|
332
|
+
* @returns {string} A unique CSS selector string.
|
|
333
|
+
*/
|
|
334
|
+
function getUniqueSelector(element) {
|
|
335
|
+
if (element.id) {
|
|
336
|
+
return `#${CSS.escape(element.id)}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const parts = [];
|
|
340
|
+
while (element && element.nodeType === Node.ELEMENT_NODE) {
|
|
341
|
+
let part = element.nodeName.toLowerCase();
|
|
342
|
+
|
|
343
|
+
// Use classList instead of className to avoid errors
|
|
344
|
+
if (element.classList && element.classList.length > 0) {
|
|
345
|
+
const classes = Array.from(element.classList)
|
|
346
|
+
.filter((cls) => cls.length > 0)
|
|
347
|
+
.map((cls) => `.${CSS.escape(cls)}`)
|
|
348
|
+
.join("");
|
|
349
|
+
part += classes;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Add nth-child if necessary
|
|
353
|
+
const parent = element.parentNode;
|
|
354
|
+
if (parent) {
|
|
355
|
+
const siblings = Array.from(parent.children).filter(
|
|
356
|
+
(sibling) => sibling.nodeName === element.nodeName,
|
|
357
|
+
);
|
|
358
|
+
if (siblings.length > 1) {
|
|
359
|
+
const index =
|
|
360
|
+
Array.prototype.indexOf.call(parent.children, element) + 1;
|
|
361
|
+
part += `:nth-child(${index})`;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
parts.unshift(part);
|
|
366
|
+
element = element.parentElement;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return parts.join(" > ");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Creates a hint marker element positioned at the top-left of the target element.
|
|
374
|
+
* @param {Element} el - The clickable DOM element.
|
|
375
|
+
* @param {string} hint - The hint identifier.
|
|
376
|
+
* @param {string} nodePath - Unique CSS selector path for the element.
|
|
377
|
+
* @returns {HTMLElement} The created hint marker element.
|
|
378
|
+
*/
|
|
379
|
+
function createHintMarker(el, hint) {
|
|
380
|
+
const rect = el.getBoundingClientRect();
|
|
381
|
+
|
|
382
|
+
const marker = document.createElement("div");
|
|
383
|
+
marker.textContent = hint;
|
|
384
|
+
marker.className = markerClass;
|
|
385
|
+
|
|
386
|
+
// Style the marker
|
|
387
|
+
Object.assign(marker.style, {
|
|
388
|
+
position: "absolute",
|
|
389
|
+
top: `${rect.top + window.scrollY}px`,
|
|
390
|
+
left: `${rect.left + window.scrollX}px`,
|
|
391
|
+
background:
|
|
392
|
+
"-webkit-gradient(linear, 0% 0%, 0% 100%, from(rgb(255, 247, 133)), to(rgb(255, 197, 66)))",
|
|
393
|
+
padding: "1px 3px 0px",
|
|
394
|
+
borderRadius: "3px",
|
|
395
|
+
border: "1px solid rgb(227, 190, 35)",
|
|
396
|
+
fontSize: "11px",
|
|
397
|
+
pointerEvents: "none",
|
|
398
|
+
zIndex: "10000",
|
|
399
|
+
whiteSpace: "nowrap",
|
|
400
|
+
overflow: "hidden",
|
|
401
|
+
boxShadow: "rgba(0, 0, 0, 0.3) 0px 3px 7px 0px",
|
|
402
|
+
letterSpacing: 0,
|
|
403
|
+
minHeight: 0,
|
|
404
|
+
lineHeight: "100%",
|
|
405
|
+
color: "rgb(48, 37, 5)",
|
|
406
|
+
fontFamily: "Helvetica, Arial, sans-serif",
|
|
407
|
+
fontWeight: "bold",
|
|
408
|
+
textShadow: "rgba(255, 255, 255, 0.6) 0px 1px 0px",
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
annotationsContainer.appendChild(marker);
|
|
412
|
+
|
|
413
|
+
return marker;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Clears all existing hint markers from the annotations container.
|
|
418
|
+
*/
|
|
419
|
+
function clearAnnotations() {
|
|
420
|
+
if (annotationsContainer) {
|
|
421
|
+
annotationsContainer.remove();
|
|
422
|
+
annotationsContainer = null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Initializes the annotations by creating the container and adding markers.
|
|
428
|
+
*/
|
|
429
|
+
function initializeAnnotations() {
|
|
430
|
+
clearAnnotations(); // Clear any existing annotations
|
|
431
|
+
|
|
432
|
+
annotationsContainer = document.createElement("div");
|
|
433
|
+
annotationsContainer.className = "annotations";
|
|
434
|
+
// Ensure the container covers the entire page
|
|
435
|
+
Object.assign(annotationsContainer.style, {
|
|
436
|
+
position: "absolute",
|
|
437
|
+
top: "0",
|
|
438
|
+
left: "0",
|
|
439
|
+
width: "100%",
|
|
440
|
+
height: "100%",
|
|
441
|
+
pointerEvents: "none", // Allow clicks to pass through
|
|
442
|
+
zIndex: "9999", // Ensure it's above other elements
|
|
443
|
+
});
|
|
444
|
+
document.body.appendChild(annotationsContainer);
|
|
445
|
+
|
|
446
|
+
const clickableElements = Array.from(document.querySelectorAll("*")).filter(
|
|
447
|
+
(el) => {
|
|
448
|
+
const isClickable = isElementClickable(el);
|
|
449
|
+
|
|
450
|
+
const originalScrollX = window.scrollX;
|
|
451
|
+
const originalScrollY = window.scrollY;
|
|
452
|
+
|
|
453
|
+
const isClickNotBlocked = isElementClickNotBlocked(el);
|
|
454
|
+
// Restore the original scroll positions
|
|
455
|
+
window.scrollTo(originalScrollX, originalScrollY);
|
|
456
|
+
|
|
457
|
+
return isClickable && isClickNotBlocked;
|
|
458
|
+
},
|
|
459
|
+
);
|
|
460
|
+
const hints = generateHintStrings(
|
|
461
|
+
hintCharacterSet,
|
|
462
|
+
Math.min(maxHints, clickableElements.length),
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
clickableElements.slice(0, maxHints).forEach((el, index) => {
|
|
466
|
+
const hint = hints[index];
|
|
467
|
+
const nodePath = getUniqueSelector(el);
|
|
468
|
+
const rect = el.getBoundingClientRect();
|
|
469
|
+
|
|
470
|
+
// Create the hint marker
|
|
471
|
+
createHintMarker(el, hint, nodePath);
|
|
472
|
+
el.style.boxShadow = `inset 0 0 0px 2px red`;
|
|
473
|
+
|
|
474
|
+
// Populate the annotations map
|
|
475
|
+
annotationsMap[hint] = {
|
|
476
|
+
node: el,
|
|
477
|
+
nodePath: nodePath,
|
|
478
|
+
rect: {
|
|
479
|
+
top: rect.top + window.scrollY,
|
|
480
|
+
left: rect.left + window.scrollX,
|
|
481
|
+
width: rect.width,
|
|
482
|
+
height: rect.height,
|
|
483
|
+
right: rect.right + window.scrollX,
|
|
484
|
+
bottom: rect.bottom + window.scrollY,
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Enables the annotations by making the annotations container visible.
|
|
492
|
+
*/
|
|
493
|
+
function enable() {
|
|
494
|
+
if (!annotationsContainer) {
|
|
495
|
+
initializeAnnotations();
|
|
496
|
+
} else {
|
|
497
|
+
annotationsContainer.style.display = "block";
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* removes all generated hints from DOM
|
|
503
|
+
*/
|
|
504
|
+
function destroy() {
|
|
505
|
+
if (annotationsContainer) {
|
|
506
|
+
Object.values(annotationsMap).forEach((annotation) => {
|
|
507
|
+
annotation.node.style.boxShadow = "none";
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
annotationsContainer.parentNode.removeChild(annotationsContainer);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Initialize annotations upon first run
|
|
515
|
+
enable();
|
|
516
|
+
|
|
517
|
+
// Return the desired object
|
|
518
|
+
return {
|
|
519
|
+
annotations: annotationsMap,
|
|
520
|
+
destroy,
|
|
521
|
+
};
|
|
522
|
+
};
|
package/dist/actions/index.d.ts
CHANGED
|
@@ -15,6 +15,14 @@ export declare class PlaywrightActions {
|
|
|
15
15
|
importPaths: string[];
|
|
16
16
|
};
|
|
17
17
|
getLastCodeLines(count: number): string[];
|
|
18
|
+
/**
|
|
19
|
+
* Function to check if the last three actions are repeated.
|
|
20
|
+
* If the steps / code are repeated then it means the generation is stuck in a loop.
|
|
21
|
+
*
|
|
22
|
+
* @return {boolean}
|
|
23
|
+
* @memberof PlaywrightActions
|
|
24
|
+
*/
|
|
25
|
+
isStuckInLoop(): boolean;
|
|
18
26
|
isComplete(): boolean;
|
|
19
27
|
}
|
|
20
28
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAIlC,OAAO,EAAE,YAAY,EAA6B,MAAM,UAAU,CAAC;AAWnE,qBAAa,iBAAiB;IAShB,OAAO,CAAC,IAAI;IARxB,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,eAAe,CAInB;IACJ,OAAO,CAAC,cAAc,CAA2B;gBAE7B,IAAI,EAAE,IAAI;IAexB,aAAa,CACjB,IAAI,oBAAa,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,KAAK,CAAC,EAAE,WAAW;IAgDrB,wBAAwB,IAAI,YAAY,EAAE;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAIlC,OAAO,EAAE,YAAY,EAA6B,MAAM,UAAU,CAAC;AAWnE,qBAAa,iBAAiB;IAShB,OAAO,CAAC,IAAI;IARxB,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,eAAe,CAInB;IACJ,OAAO,CAAC,cAAc,CAA2B;gBAE7B,IAAI,EAAE,IAAI;IAexB,aAAa,CACjB,IAAI,oBAAa,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,KAAK,CAAC,EAAE,WAAW;IAgDrB,wBAAwB,IAAI,YAAY,EAAE;IAkB1C,sBAAsB,IAAI,YAAY,EAAE;IAUxC,YAAY,IAAI;QACd,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,EAAE,CAAC;KACvB;IAUD,gBAAgB,CAAC,KAAK,EAAE,MAAM;IAO9B;;;;;;OAMG;IACH,aAAa,IAAI,OAAO;IAQxB,UAAU;CAUX"}
|
package/dist/actions/index.js
CHANGED
|
@@ -88,7 +88,6 @@ class PlaywrightActions {
|
|
|
88
88
|
assert_1.assertTextVisibilityActionGenerator,
|
|
89
89
|
reload_page_1.reloadActionGenerator,
|
|
90
90
|
text_content_1.textContentActionGenerator,
|
|
91
|
-
skill_1.skillActionGenerator,
|
|
92
91
|
]
|
|
93
92
|
.map((a) => a(this.page, {
|
|
94
93
|
stateVariables: this.stateVariables,
|
|
@@ -115,6 +114,19 @@ class PlaywrightActions {
|
|
|
115
114
|
const actions = this.recordedActions.filter((a) => a.name !== done_1.PLAYWRIGHT_DONE_ACTION_NAME);
|
|
116
115
|
return actions.slice(-count).map((a) => a.code);
|
|
117
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Function to check if the last three actions are repeated.
|
|
119
|
+
* If the steps / code are repeated then it means the generation is stuck in a loop.
|
|
120
|
+
*
|
|
121
|
+
* @return {boolean}
|
|
122
|
+
* @memberof PlaywrightActions
|
|
123
|
+
*/
|
|
124
|
+
isStuckInLoop() {
|
|
125
|
+
const lastThreeLinesOfCode = this.getLastCodeLines(3);
|
|
126
|
+
const areLastActionsRepeatitive = lastThreeLinesOfCode.length === 3 &&
|
|
127
|
+
lastThreeLinesOfCode.every((a) => a === lastThreeLinesOfCode[0]);
|
|
128
|
+
return areLastActionsRepeatitive;
|
|
129
|
+
}
|
|
118
130
|
isComplete() {
|
|
119
131
|
const [doneAction] = this.recordedActions.filter((a) => a.name === done_1.PLAYWRIGHT_DONE_ACTION_NAME);
|
|
120
132
|
// filter out done action from recorded actions aftet execution is marked complete
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/agent/browsing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIhD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAMnD,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC,GAAG;IACjE,YAAY,CAAC,EAAE;QACb,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;KAC9B,CAAC;CACH,CAAC;AAEF,wBAAsB,6BAA6B,CAAC,EAClD,KAAK,EACL,MAAM,EACN,MAAM,EACN,IAAI,EACJ,OAAO,EACP,GAAG,EACH,OAAO,GACR,EAAE;IACD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,oBAAoB,CAAC;IAC9B,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,EAAE,iBAAiB,CAAC;CAC5B,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/agent/browsing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIhD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAMnD,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC,GAAG;IACjE,YAAY,CAAC,EAAE;QACb,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;KAC9B,CAAC;CACH,CAAC;AAEF,wBAAsB,6BAA6B,CAAC,EAClD,KAAK,EACL,MAAM,EACN,MAAM,EACN,IAAI,EACJ,OAAO,EACP,GAAG,EACH,OAAO,GACR,EAAE;IACD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,oBAAoB,CAAC;IAC9B,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,EAAE,iBAAiB,CAAC;CAC5B,iBAqIA"}
|
|
@@ -122,10 +122,8 @@ async function executeTaskUsingBrowsingAgent({ trace, action, logger, page, opti
|
|
|
122
122
|
const lastThreeActions = executedActions.slice(-3);
|
|
123
123
|
const lastThreeActionsFailed = lastThreeActions.every((a) => a.isError);
|
|
124
124
|
// get last 3 lines of code
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
lastThreeLinesOfCode.every((a) => a === lastThreeLinesOfCode[0]);
|
|
128
|
-
if (lastThreeActionsFailed || areLastActionsRepeatitive) {
|
|
125
|
+
const isStuckInLoop = actions.isStuckInLoop();
|
|
126
|
+
if (lastThreeActionsFailed || isStuckInLoop) {
|
|
129
127
|
// TODO: this should be sent to dashboard
|
|
130
128
|
const error = "Agent is not able to figure out next browser action, ending retries";
|
|
131
129
|
logger.error(error);
|
|
@@ -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;
|
|
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;AAgBvD,OAAO,EAAe,aAAa,EAAE,MAAM,aAAa,CAAC;AAKzD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,MAAM,CAKhD;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,UAIvD;AA4ED;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,aAAa,EACxB,KAAK,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,CAAC,CAuDjB;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"}
|
|
@@ -8,6 +8,7 @@ const llm_1 = require("@empiricalrun/llm");
|
|
|
8
8
|
const child_process_1 = require("child_process");
|
|
9
9
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
10
|
const minimatch_1 = require("minimatch");
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
11
12
|
const ts_morph_1 = require("ts-morph");
|
|
12
13
|
const api_1 = __importDefault(require("tsx/cjs/api"));
|
|
13
14
|
const logger_1 = require("../../bin/logger");
|
|
@@ -146,12 +147,19 @@ function newContentsWithTestOnly(existingContents, originalTestBlock, updatedTes
|
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
149
|
async function injectPwLocatorGenerator(page) {
|
|
149
|
-
const
|
|
150
|
+
const pathToInstalledTestGen = require.resolve(".").split("dist")[0];
|
|
151
|
+
const annotateElementPath = path_1.default.join(pathToInstalledTestGen, "dist", "browser-injected-scripts", "annotate-elements.js");
|
|
152
|
+
if (!fs_extra_1.default.existsSync(annotateElementPath)) {
|
|
153
|
+
throw new Error(`annotate-elements.js not found at path: ${annotateElementPath}`);
|
|
154
|
+
}
|
|
155
|
+
const remoteScriptResponses = await Promise.all([
|
|
150
156
|
"https://assets-test.empirical.run/pw-selector.js",
|
|
151
157
|
"https://code.jquery.com/jquery-3.7.1.min.js",
|
|
152
|
-
"https://assets-test.empirical.run/annotate-elements.js",
|
|
153
158
|
].map((url) => fetch(url)));
|
|
154
|
-
const scripts = await Promise.all(
|
|
159
|
+
const scripts = await Promise.all([
|
|
160
|
+
...remoteScriptResponses.map((r) => r.text()),
|
|
161
|
+
fs_extra_1.default.readFile(annotateElementPath, "utf-8"),
|
|
162
|
+
]);
|
|
155
163
|
page.on("load", async () => {
|
|
156
164
|
// add script for subsequent page load events
|
|
157
165
|
try {
|
|
@@ -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;AAYlD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EACL,oBAAoB,EAErB,MAAM,aAAa,CAAC;AAQrB,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,GACjB,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;CACnB,2FAwFA;AAGD,wBAAsB,0BAA0B,CAAC,EAC/C,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,OAAO,GACR,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,oBAAoB,CAAC;CAC/B;;;
|
|
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;AAYlD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EACL,oBAAoB,EAErB,MAAM,aAAa,CAAC;AAQrB,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,GACjB,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;CACnB,2FAwFA;AAGD,wBAAsB,0BAA0B,CAAC,EAC/C,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,OAAO,GACR,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,oBAAoB,CAAC;CAC/B;;;GA4RA"}
|
package/dist/agent/master/run.js
CHANGED
|
@@ -102,15 +102,17 @@ async function createTestUsingMasterAgent({ task, page, testCase, options, }) {
|
|
|
102
102
|
const useHints = options?.useHints || false;
|
|
103
103
|
const logger = new logger_1.CustomLogger({ useReporter: false });
|
|
104
104
|
const testgenUpdatesReporter = new reporter_1.TestGenUpdatesReporter();
|
|
105
|
+
const session = (0, session_1.getSessionDetails)();
|
|
105
106
|
// add timeout for the page to settle in
|
|
106
107
|
await page.waitForTimeout(3000);
|
|
107
108
|
const trace = llm_1.langfuseInstance?.trace({
|
|
108
109
|
name: "test-generator",
|
|
109
110
|
id: crypto.randomUUID(),
|
|
110
|
-
version:
|
|
111
|
+
version: session.version,
|
|
111
112
|
metadata: {
|
|
112
|
-
generationId:
|
|
113
|
-
sessionId:
|
|
113
|
+
generationId: session.generationId,
|
|
114
|
+
sessionId: session.sessionId,
|
|
115
|
+
testUrl: session.testUrl,
|
|
114
116
|
},
|
|
115
117
|
tags: [
|
|
116
118
|
options.metadata?.projectName,
|
|
@@ -265,7 +267,10 @@ async function createTestUsingMasterAgent({ task, page, testCase, options, }) {
|
|
|
265
267
|
await actions.executeAction(currentToolCall.function.name, {
|
|
266
268
|
...JSON.parse(currentToolCall.function.arguments),
|
|
267
269
|
...args,
|
|
268
|
-
});
|
|
270
|
+
}, masterAgentActionSpan);
|
|
271
|
+
}
|
|
272
|
+
if (actions.isStuckInLoop()) {
|
|
273
|
+
throw new Error("Agent is not able to figure out next action when using hints");
|
|
269
274
|
}
|
|
270
275
|
}
|
|
271
276
|
else {
|
|
@@ -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,MAAM,mBAAmB,CAAC;AAExC,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAIlC,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;6BAMF;QACvB,MAAM,EAAE,MAAM,CAAC;QACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC5B;0BACqB,OAAO,MAAM,EAAE,GAAG,CAAC;UACnC,IAAI;SACL,GAAG;MACN,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,MAAM,mBAAmB,CAAC;AAExC,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAIlC,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;6BAMF;QACvB,MAAM,EAAE,MAAM,CAAC;QACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC5B;0BACqB,OAAO,MAAM,EAAE,GAAG,CAAC;UACnC,IAAI;SACL,GAAG;MACN,QAAQ;IACV,sBAAsB,EAAE,OAAO,CAAC;IAChC,wBAAwB,EAAE,OAAO,qBAAqB,GAAG,SAAS,CAAC;CACpE,CA6EA,CAAC"}
|
|
@@ -73,10 +73,7 @@ const triggerHintsFlow = async ({ outputFromGetNextAction, generatedAnnotations,
|
|
|
73
73
|
],
|
|
74
74
|
},
|
|
75
75
|
],
|
|
76
|
-
tools: actions
|
|
77
|
-
.getBrowsingActionSchemas()
|
|
78
|
-
.filter((action) => action.function.name !== "skill_usage"),
|
|
79
|
-
model: "gpt-4o-2024-08-06",
|
|
76
|
+
tools: actions.getBrowsingActionSchemas(),
|
|
80
77
|
modelParameters: {
|
|
81
78
|
temperature: 0.5,
|
|
82
79
|
max_completion_tokens: 4000,
|
package/dist/bin/index.js
CHANGED
|
@@ -55,6 +55,13 @@ async function runAgent(testGenConfig) {
|
|
|
55
55
|
testGenConfig.options?.metadata.environment || "",
|
|
56
56
|
].filter((s) => !!s),
|
|
57
57
|
});
|
|
58
|
+
trace?.update({
|
|
59
|
+
metadata: {
|
|
60
|
+
generationId: session.generationId,
|
|
61
|
+
sessionId: session.sessionId,
|
|
62
|
+
testUrl: session.testUrl,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
58
65
|
if (trace) {
|
|
59
66
|
try {
|
|
60
67
|
await new reporter_1.TestGenUpdatesReporter().sendAgentTraceUrl(trace.getTraceUrl());
|
|
@@ -104,6 +111,8 @@ async function runAgent(testGenConfig) {
|
|
|
104
111
|
(0, session_1.setSessionDetails)({
|
|
105
112
|
sessionId: testGenConfig.options?.metadata.testSessionId,
|
|
106
113
|
generationId: testGenConfig.options?.metadata.generationId,
|
|
114
|
+
testCaseId: testGenConfig.testCase.id,
|
|
115
|
+
projectRepoName: testGenConfig.options?.metadata.projectRepoName,
|
|
107
116
|
});
|
|
108
117
|
let testGenFailed = false;
|
|
109
118
|
try {
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
/* eslint-disable no-undef */
|
|
2
|
+
/**
|
|
3
|
+
* Annotates all clickable elements on the page with unique hint markers.
|
|
4
|
+
* Returns an object containing annotations and methods to enable/disable them.
|
|
5
|
+
*
|
|
6
|
+
* @param {Object} options - Configuration options for hint markers.
|
|
7
|
+
* @param {string} options.hintCharacterSet - Characters to use for generating hint identifiers.
|
|
8
|
+
* @param {number} options.maxHints - Maximum number of hints to generate.
|
|
9
|
+
* @param {string} options.markerClass - CSS class to apply to hint markers.
|
|
10
|
+
* @returns {Object} An object containing annotations map and enable/disable methods.
|
|
11
|
+
*/
|
|
12
|
+
window.annotateClickableElements = function annotateClickableElements(
|
|
13
|
+
options = {},
|
|
14
|
+
) {
|
|
15
|
+
const {
|
|
16
|
+
hintCharacterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", // Default set of characters for hints
|
|
17
|
+
maxHints = 1000, // Maximum number of hints to generate
|
|
18
|
+
markerClass = "hint-marker", // CSS class for markers
|
|
19
|
+
} = options;
|
|
20
|
+
|
|
21
|
+
const annotationsMap = {};
|
|
22
|
+
let annotationsContainer = null;
|
|
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) {
|
|
34
|
+
const rect = element.getBoundingClientRect();
|
|
35
|
+
|
|
36
|
+
// Calculate the center point of the element
|
|
37
|
+
const centerX = rect.left + rect.width / 2;
|
|
38
|
+
const centerY = rect.top + rect.height / 2;
|
|
39
|
+
|
|
40
|
+
// check if element is within the viewport
|
|
41
|
+
if (
|
|
42
|
+
centerX < 0 ||
|
|
43
|
+
centerY < 0 ||
|
|
44
|
+
centerX > (window.innerWidth || document.documentElement.clientWidth) ||
|
|
45
|
+
centerY > (window.innerHeight || document.documentElement.clientHeight)
|
|
46
|
+
) {
|
|
47
|
+
// Determine the viewport dimensions
|
|
48
|
+
const viewportWidth =
|
|
49
|
+
window.innerWidth || document.documentElement.clientWidth;
|
|
50
|
+
const viewportHeight =
|
|
51
|
+
window.innerHeight || document.documentElement.clientHeight;
|
|
52
|
+
|
|
53
|
+
// Calculate the new scroll positions to bring the element into the center of the viewport
|
|
54
|
+
const newScrollX = centerX - viewportWidth / 2;
|
|
55
|
+
const newScrollY = centerY - viewportHeight / 2;
|
|
56
|
+
|
|
57
|
+
// Scroll the window to the new positions
|
|
58
|
+
window.scrollTo({
|
|
59
|
+
top: newScrollY,
|
|
60
|
+
left: newScrollX,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const newRect = element.getBoundingClientRect();
|
|
64
|
+
const newCenterX = newRect.left + newRect.width / 2;
|
|
65
|
+
const newCenterY = newRect.top + newRect.height / 2;
|
|
66
|
+
const topElement = document.elementFromPoint(newCenterX, newCenterY);
|
|
67
|
+
return element.contains(topElement);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Retrieve the topmost element at the center point
|
|
71
|
+
const topElement = document.elementFromPoint(centerX, centerY);
|
|
72
|
+
|
|
73
|
+
// Check if the topmost element is the target element or one of its descendants
|
|
74
|
+
return element.contains(topElement);
|
|
75
|
+
}
|
|
76
|
+
|
|
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
|
+
function generateHintStrings(charset, max) {
|
|
85
|
+
const hints = [];
|
|
86
|
+
let length = 1;
|
|
87
|
+
|
|
88
|
+
while (hints.length < max) {
|
|
89
|
+
const combos = cartesianProduct(Array(length).fill(charset.split("")));
|
|
90
|
+
for (const combo of combos) {
|
|
91
|
+
hints.push(combo.join(""));
|
|
92
|
+
if (hints.length >= max) break;
|
|
93
|
+
}
|
|
94
|
+
length++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return hints;
|
|
98
|
+
}
|
|
99
|
+
|
|
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
|
+
function cartesianProduct(arrays) {
|
|
106
|
+
return arrays.reduce(
|
|
107
|
+
(acc, curr) =>
|
|
108
|
+
acc
|
|
109
|
+
.map((a) => curr.map((b) => a.concat([b])))
|
|
110
|
+
.reduce((a, b) => a.concat(b), []),
|
|
111
|
+
[[]],
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
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
|
+
*/
|
|
121
|
+
function isElementClickable(element) {
|
|
122
|
+
if (!(element instanceof Element)) return false;
|
|
123
|
+
|
|
124
|
+
const tagName = element.tagName.toLowerCase();
|
|
125
|
+
let isClickable = false;
|
|
126
|
+
|
|
127
|
+
// Check for aria-disabled
|
|
128
|
+
const ariaDisabled = element.getAttribute("aria-disabled");
|
|
129
|
+
if (ariaDisabled && ["", "true"].includes(ariaDisabled.toLowerCase())) {
|
|
130
|
+
return false; // Element should not be clickable if aria-disabled is true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check for visibility
|
|
134
|
+
const style = window.getComputedStyle(element);
|
|
135
|
+
if (
|
|
136
|
+
style.display === "none" ||
|
|
137
|
+
style.visibility === "hidden" ||
|
|
138
|
+
parseFloat(style.opacity) === 0 ||
|
|
139
|
+
style.pointerEvents === "none"
|
|
140
|
+
) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check if element is disabled (for applicable elements)
|
|
145
|
+
if (element.disabled) return false;
|
|
146
|
+
|
|
147
|
+
// Check for AngularJS click handlers
|
|
148
|
+
if (!isElementClickable._checkForAngularJs) {
|
|
149
|
+
isElementClickable._checkForAngularJs = (function () {
|
|
150
|
+
const angularElements = document.getElementsByClassName("ng-scope");
|
|
151
|
+
if (angularElements.length === 0) {
|
|
152
|
+
return () => false;
|
|
153
|
+
} else {
|
|
154
|
+
const ngAttributes = [];
|
|
155
|
+
for (const prefix of ["", "data-", "x-"]) {
|
|
156
|
+
for (const separator of ["-", ":", "_"]) {
|
|
157
|
+
ngAttributes.push(`${prefix}ng${separator}click`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return function (el) {
|
|
161
|
+
for (const attribute of ngAttributes) {
|
|
162
|
+
if (el.hasAttribute(attribute)) return true;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
})();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!isClickable && isElementClickable._checkForAngularJs(element)) {
|
|
171
|
+
isClickable = true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check for onclick attribute or listener
|
|
175
|
+
if (
|
|
176
|
+
element.hasAttribute("onclick") ||
|
|
177
|
+
typeof element.onclick === "function"
|
|
178
|
+
) {
|
|
179
|
+
isClickable = true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check for jsaction attribute (commonly used in frameworks like Google's)
|
|
183
|
+
if (!isClickable && element.hasAttribute("jsaction")) {
|
|
184
|
+
const jsactionRules = element.getAttribute("jsaction").split(";");
|
|
185
|
+
for (const jsactionRule of jsactionRules) {
|
|
186
|
+
const ruleSplit = jsactionRule.trim().split(":");
|
|
187
|
+
if (ruleSplit.length >= 1 && ruleSplit.length <= 2) {
|
|
188
|
+
const [eventType] = ruleSplit[0].trim().split(".");
|
|
189
|
+
if (eventType === "click") {
|
|
190
|
+
isClickable = true;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check for role attributes that imply clickability
|
|
198
|
+
if (!isClickable) {
|
|
199
|
+
const role = element.getAttribute("role");
|
|
200
|
+
const clickableRoles = [
|
|
201
|
+
"button",
|
|
202
|
+
"tab",
|
|
203
|
+
"link",
|
|
204
|
+
"checkbox",
|
|
205
|
+
"menuitem",
|
|
206
|
+
"menuitemcheckbox",
|
|
207
|
+
"menuitemradio",
|
|
208
|
+
"radio",
|
|
209
|
+
"switch",
|
|
210
|
+
];
|
|
211
|
+
if (role && clickableRoles.includes(role.toLowerCase())) {
|
|
212
|
+
isClickable = true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check for contentEditable
|
|
217
|
+
if (!isClickable) {
|
|
218
|
+
const contentEditable = element.getAttribute("contentEditable");
|
|
219
|
+
if (
|
|
220
|
+
contentEditable != null &&
|
|
221
|
+
["", "contenteditable", "true"].includes(contentEditable.toLowerCase())
|
|
222
|
+
) {
|
|
223
|
+
isClickable = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Tag-specific clickability
|
|
228
|
+
const focusableTags = [
|
|
229
|
+
"a",
|
|
230
|
+
"button",
|
|
231
|
+
"input",
|
|
232
|
+
"select",
|
|
233
|
+
"textarea",
|
|
234
|
+
"object",
|
|
235
|
+
"embed",
|
|
236
|
+
"label",
|
|
237
|
+
"details",
|
|
238
|
+
];
|
|
239
|
+
if (focusableTags.includes(tagName)) {
|
|
240
|
+
switch (tagName) {
|
|
241
|
+
case "a":
|
|
242
|
+
// Ensure it's not just a named anchor without href
|
|
243
|
+
if (element.hasAttribute("href")) {
|
|
244
|
+
isClickable = true;
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
case "input": {
|
|
248
|
+
const type = (element.getAttribute("type") || "").toLowerCase();
|
|
249
|
+
if (
|
|
250
|
+
type !== "hidden" &&
|
|
251
|
+
!element.disabled &&
|
|
252
|
+
!(element.readOnly && isInputSelectable(element))
|
|
253
|
+
) {
|
|
254
|
+
isClickable = true;
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case "textarea":
|
|
259
|
+
if (!element.disabled && !element.readOnly) {
|
|
260
|
+
isClickable = true;
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
case "button":
|
|
264
|
+
case "select":
|
|
265
|
+
if (!element.disabled) {
|
|
266
|
+
isClickable = true;
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
case "object":
|
|
270
|
+
case "embed":
|
|
271
|
+
isClickable = true;
|
|
272
|
+
break;
|
|
273
|
+
case "label":
|
|
274
|
+
if (element.control && !element.control.disabled) {
|
|
275
|
+
isClickable = true;
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
case "details":
|
|
279
|
+
isClickable = true;
|
|
280
|
+
break;
|
|
281
|
+
default:
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check for class names containing 'button' as a possible click target
|
|
287
|
+
if (!isClickable) {
|
|
288
|
+
const className = element.getAttribute("class");
|
|
289
|
+
if (className && className.toLowerCase().includes("button")) {
|
|
290
|
+
isClickable = true;
|
|
291
|
+
} else if (element.classList.contains("cursor-pointer")) {
|
|
292
|
+
isClickable = true;
|
|
293
|
+
} else if (element.style.cursor === "pointer") {
|
|
294
|
+
isClickable = true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check for tabindex
|
|
299
|
+
if (!isClickable) {
|
|
300
|
+
const tabIndexValue = element.getAttribute("tabindex");
|
|
301
|
+
const tabIndex = tabIndexValue ? parseInt(tabIndexValue) : -1;
|
|
302
|
+
if (tabIndex >= 0 && !isNaN(tabIndex)) {
|
|
303
|
+
isClickable = true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return isClickable;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Helper function to determine if an input is selectable (e.g., text, search).
|
|
312
|
+
* @param {Element} input - The input element.
|
|
313
|
+
* @returns {boolean} True if the input type is selectable.
|
|
314
|
+
*/
|
|
315
|
+
function isInputSelectable(input) {
|
|
316
|
+
const selectableTypes = [
|
|
317
|
+
"text",
|
|
318
|
+
"search",
|
|
319
|
+
"password",
|
|
320
|
+
"url",
|
|
321
|
+
"email",
|
|
322
|
+
"number",
|
|
323
|
+
"tel",
|
|
324
|
+
];
|
|
325
|
+
const type = (input.getAttribute("type") || "").toLowerCase();
|
|
326
|
+
return selectableTypes.includes(type);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Generates a unique CSS selector path for a given element.
|
|
331
|
+
* @param {Element} element - The DOM element.
|
|
332
|
+
* @returns {string} A unique CSS selector string.
|
|
333
|
+
*/
|
|
334
|
+
function getUniqueSelector(element) {
|
|
335
|
+
if (element.id) {
|
|
336
|
+
return `#${CSS.escape(element.id)}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const parts = [];
|
|
340
|
+
while (element && element.nodeType === Node.ELEMENT_NODE) {
|
|
341
|
+
let part = element.nodeName.toLowerCase();
|
|
342
|
+
|
|
343
|
+
// Use classList instead of className to avoid errors
|
|
344
|
+
if (element.classList && element.classList.length > 0) {
|
|
345
|
+
const classes = Array.from(element.classList)
|
|
346
|
+
.filter((cls) => cls.length > 0)
|
|
347
|
+
.map((cls) => `.${CSS.escape(cls)}`)
|
|
348
|
+
.join("");
|
|
349
|
+
part += classes;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Add nth-child if necessary
|
|
353
|
+
const parent = element.parentNode;
|
|
354
|
+
if (parent) {
|
|
355
|
+
const siblings = Array.from(parent.children).filter(
|
|
356
|
+
(sibling) => sibling.nodeName === element.nodeName,
|
|
357
|
+
);
|
|
358
|
+
if (siblings.length > 1) {
|
|
359
|
+
const index =
|
|
360
|
+
Array.prototype.indexOf.call(parent.children, element) + 1;
|
|
361
|
+
part += `:nth-child(${index})`;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
parts.unshift(part);
|
|
366
|
+
element = element.parentElement;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return parts.join(" > ");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Creates a hint marker element positioned at the top-left of the target element.
|
|
374
|
+
* @param {Element} el - The clickable DOM element.
|
|
375
|
+
* @param {string} hint - The hint identifier.
|
|
376
|
+
* @param {string} nodePath - Unique CSS selector path for the element.
|
|
377
|
+
* @returns {HTMLElement} The created hint marker element.
|
|
378
|
+
*/
|
|
379
|
+
function createHintMarker(el, hint) {
|
|
380
|
+
const rect = el.getBoundingClientRect();
|
|
381
|
+
|
|
382
|
+
const marker = document.createElement("div");
|
|
383
|
+
marker.textContent = hint;
|
|
384
|
+
marker.className = markerClass;
|
|
385
|
+
|
|
386
|
+
// Style the marker
|
|
387
|
+
Object.assign(marker.style, {
|
|
388
|
+
position: "absolute",
|
|
389
|
+
top: `${rect.top + window.scrollY}px`,
|
|
390
|
+
left: `${rect.left + window.scrollX}px`,
|
|
391
|
+
background:
|
|
392
|
+
"-webkit-gradient(linear, 0% 0%, 0% 100%, from(rgb(255, 247, 133)), to(rgb(255, 197, 66)))",
|
|
393
|
+
padding: "1px 3px 0px",
|
|
394
|
+
borderRadius: "3px",
|
|
395
|
+
border: "1px solid rgb(227, 190, 35)",
|
|
396
|
+
fontSize: "11px",
|
|
397
|
+
pointerEvents: "none",
|
|
398
|
+
zIndex: "10000",
|
|
399
|
+
whiteSpace: "nowrap",
|
|
400
|
+
overflow: "hidden",
|
|
401
|
+
boxShadow: "rgba(0, 0, 0, 0.3) 0px 3px 7px 0px",
|
|
402
|
+
letterSpacing: 0,
|
|
403
|
+
minHeight: 0,
|
|
404
|
+
lineHeight: "100%",
|
|
405
|
+
color: "rgb(48, 37, 5)",
|
|
406
|
+
fontFamily: "Helvetica, Arial, sans-serif",
|
|
407
|
+
fontWeight: "bold",
|
|
408
|
+
textShadow: "rgba(255, 255, 255, 0.6) 0px 1px 0px",
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
annotationsContainer.appendChild(marker);
|
|
412
|
+
|
|
413
|
+
return marker;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Clears all existing hint markers from the annotations container.
|
|
418
|
+
*/
|
|
419
|
+
function clearAnnotations() {
|
|
420
|
+
if (annotationsContainer) {
|
|
421
|
+
annotationsContainer.remove();
|
|
422
|
+
annotationsContainer = null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Initializes the annotations by creating the container and adding markers.
|
|
428
|
+
*/
|
|
429
|
+
function initializeAnnotations() {
|
|
430
|
+
clearAnnotations(); // Clear any existing annotations
|
|
431
|
+
|
|
432
|
+
annotationsContainer = document.createElement("div");
|
|
433
|
+
annotationsContainer.className = "annotations";
|
|
434
|
+
// Ensure the container covers the entire page
|
|
435
|
+
Object.assign(annotationsContainer.style, {
|
|
436
|
+
position: "absolute",
|
|
437
|
+
top: "0",
|
|
438
|
+
left: "0",
|
|
439
|
+
width: "100%",
|
|
440
|
+
height: "100%",
|
|
441
|
+
pointerEvents: "none", // Allow clicks to pass through
|
|
442
|
+
zIndex: "9999", // Ensure it's above other elements
|
|
443
|
+
});
|
|
444
|
+
document.body.appendChild(annotationsContainer);
|
|
445
|
+
|
|
446
|
+
const clickableElements = Array.from(document.querySelectorAll("*")).filter(
|
|
447
|
+
(el) => {
|
|
448
|
+
const isClickable = isElementClickable(el);
|
|
449
|
+
|
|
450
|
+
const originalScrollX = window.scrollX;
|
|
451
|
+
const originalScrollY = window.scrollY;
|
|
452
|
+
|
|
453
|
+
const isClickNotBlocked = isElementClickNotBlocked(el);
|
|
454
|
+
// Restore the original scroll positions
|
|
455
|
+
window.scrollTo(originalScrollX, originalScrollY);
|
|
456
|
+
|
|
457
|
+
return isClickable && isClickNotBlocked;
|
|
458
|
+
},
|
|
459
|
+
);
|
|
460
|
+
const hints = generateHintStrings(
|
|
461
|
+
hintCharacterSet,
|
|
462
|
+
Math.min(maxHints, clickableElements.length),
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
clickableElements.slice(0, maxHints).forEach((el, index) => {
|
|
466
|
+
const hint = hints[index];
|
|
467
|
+
const nodePath = getUniqueSelector(el);
|
|
468
|
+
const rect = el.getBoundingClientRect();
|
|
469
|
+
|
|
470
|
+
// Create the hint marker
|
|
471
|
+
createHintMarker(el, hint, nodePath);
|
|
472
|
+
el.style.boxShadow = `inset 0 0 0px 2px red`;
|
|
473
|
+
|
|
474
|
+
// Populate the annotations map
|
|
475
|
+
annotationsMap[hint] = {
|
|
476
|
+
node: el,
|
|
477
|
+
nodePath: nodePath,
|
|
478
|
+
rect: {
|
|
479
|
+
top: rect.top + window.scrollY,
|
|
480
|
+
left: rect.left + window.scrollX,
|
|
481
|
+
width: rect.width,
|
|
482
|
+
height: rect.height,
|
|
483
|
+
right: rect.right + window.scrollX,
|
|
484
|
+
bottom: rect.bottom + window.scrollY,
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Enables the annotations by making the annotations container visible.
|
|
492
|
+
*/
|
|
493
|
+
function enable() {
|
|
494
|
+
if (!annotationsContainer) {
|
|
495
|
+
initializeAnnotations();
|
|
496
|
+
} else {
|
|
497
|
+
annotationsContainer.style.display = "block";
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* removes all generated hints from DOM
|
|
503
|
+
*/
|
|
504
|
+
function destroy() {
|
|
505
|
+
if (annotationsContainer) {
|
|
506
|
+
Object.values(annotationsMap).forEach((annotation) => {
|
|
507
|
+
annotation.node.style.boxShadow = "none";
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
annotationsContainer.parentNode.removeChild(annotationsContainer);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Initialize annotations upon first run
|
|
515
|
+
enable();
|
|
516
|
+
|
|
517
|
+
// Return the desired object
|
|
518
|
+
return {
|
|
519
|
+
annotations: annotationsMap,
|
|
520
|
+
destroy,
|
|
521
|
+
};
|
|
522
|
+
};
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAclC,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAclC,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,iBAsCxD"}
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,8 @@ async function createTest(task, page) {
|
|
|
30
30
|
(0, session_1.setSessionDetails)({
|
|
31
31
|
sessionId: testGenConfig.options?.metadata.testSessionId,
|
|
32
32
|
generationId: testGenConfig.options?.metadata.generationId,
|
|
33
|
+
testCaseId: testGenConfig.testCase.id,
|
|
34
|
+
projectRepoName: testGenConfig.options?.metadata.projectRepoName,
|
|
33
35
|
});
|
|
34
36
|
const fileService = new client_1.default(Number(port));
|
|
35
37
|
const { code, importPaths } = await (0, run_1.createTestUsingMasterAgent)({
|
package/dist/session/index.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
declare function getSessionDetails(): {
|
|
2
2
|
generationId: number | undefined;
|
|
3
3
|
sessionId: number | undefined;
|
|
4
|
+
testUrl: string | undefined;
|
|
4
5
|
version: string;
|
|
5
6
|
};
|
|
6
|
-
export declare function setSessionDetails({ sessionId, generationId, }: {
|
|
7
|
+
export declare function setSessionDetails({ sessionId, generationId, testCaseId, projectRepoName, }: {
|
|
7
8
|
sessionId: number;
|
|
8
9
|
generationId: number;
|
|
10
|
+
testCaseId: number;
|
|
11
|
+
projectRepoName: string;
|
|
9
12
|
}): void;
|
|
10
13
|
export declare function shouldStopSession(): Promise<boolean>;
|
|
11
14
|
export declare function getSessionState(): Promise<"started" | "completed" | "request_complete">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/session/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/session/index.ts"],"names":[],"mappings":"AAkBA,iBAAS,iBAAiB;;;;;EAOzB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,YAAY,EACZ,UAAU,EACV,eAAe,GAChB,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;CACzB,QAIA;AAED,wBAAsB,iBAAiB,qBAGtC;AAED,wBAAsB,eAAe,0DAqBpC;AAED,wBAAsB,UAAU,kBAqB/B;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
|
package/dist/session/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const sessionDetails = {
|
|
|
9
9
|
sessionId: undefined,
|
|
10
10
|
version: package_json_1.default.version,
|
|
11
11
|
generationId: undefined,
|
|
12
|
+
testUrl: undefined,
|
|
12
13
|
};
|
|
13
14
|
const DASHBOARD_DOMAIN = process.env.DASHBOARD_DOMAIN ||
|
|
14
15
|
(process.env.CI === "true" ? "https://dash.empirical.run" : "");
|
|
@@ -16,13 +17,15 @@ function getSessionDetails() {
|
|
|
16
17
|
return {
|
|
17
18
|
generationId: sessionDetails.generationId,
|
|
18
19
|
sessionId: sessionDetails.sessionId,
|
|
20
|
+
testUrl: sessionDetails.testUrl,
|
|
19
21
|
version: package_json_1.default.version,
|
|
20
22
|
};
|
|
21
23
|
}
|
|
22
24
|
exports.getSessionDetails = getSessionDetails;
|
|
23
|
-
function setSessionDetails({ sessionId, generationId, }) {
|
|
25
|
+
function setSessionDetails({ sessionId, generationId, testCaseId, projectRepoName, }) {
|
|
24
26
|
sessionDetails.sessionId = sessionId;
|
|
25
27
|
sessionDetails.generationId = generationId;
|
|
28
|
+
sessionDetails.testUrl = `${process.env.DASHBOARD_DOMAIN ?? ""}/${projectRepoName}/test-cases/${testCaseId}`;
|
|
26
29
|
}
|
|
27
30
|
exports.setSessionDetails = setSessionDetails;
|
|
28
31
|
async function shouldStopSession() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empiricalrun/test-gen",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.4",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/",
|
|
6
6
|
"access": "public"
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"dev": "tsc --build --watch",
|
|
60
|
-
"build": "tsc --build",
|
|
60
|
+
"build": "tsc --build && cp -r browser-injected-scripts dist",
|
|
61
61
|
"clean": "tsc --build --clean",
|
|
62
62
|
"lint": "eslint .",
|
|
63
63
|
"test": "vitest run",
|