@empiricalrun/test-gen 0.35.1 → 0.35.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @empiricalrun/test-gen
2
2
 
3
+ ## 0.35.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 19f5344: fix: added test url in trace metadata
8
+
9
+ ## 0.35.2
10
+
11
+ ### Patch Changes
12
+
13
+ - 2821db3: fix: handle agent getting stuck in loop
14
+ - 0fe1f6e: fix: load annotate-elements script from disk
15
+
3
16
  ## 0.35.1
4
17
 
5
18
  ### Patch Changes
@@ -0,0 +1,475 @@
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 an element is visible to the user.
26
+ * It considers not just the viewport but also the scrollable parent.
27
+ * @param {Element} element - The DOM element to check.
28
+ * @returns {boolean} True if the element is visible to the user.
29
+ */
30
+ function isElementVisibleToUser(element) {
31
+ const rect = element.getBoundingClientRect();
32
+ return (
33
+ rect.top >= 0 &&
34
+ rect.left >= 0 &&
35
+ rect.bottom <=
36
+ (window.innerHeight || document.documentElement.clientHeight) &&
37
+ rect.right <= (window.innerWidth || document.documentElement.clientWidth)
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Generates a sequence of hint strings based on the provided character set.
43
+ * For example, with 'abc' as the set and maxHints 5, it generates: 'a', 'b', 'c', 'aa', 'ab'
44
+ * @param {string} charset - Characters to use.
45
+ * @param {number} max - Maximum number of hints to generate.
46
+ * @returns {string[]} Array of hint strings.
47
+ */
48
+ function generateHintStrings(charset, max) {
49
+ const hints = [];
50
+ let length = 1;
51
+
52
+ while (hints.length < max) {
53
+ const combos = cartesianProduct(Array(length).fill(charset.split("")));
54
+ for (const combo of combos) {
55
+ hints.push(combo.join(""));
56
+ if (hints.length >= max) break;
57
+ }
58
+ length++;
59
+ }
60
+
61
+ return hints;
62
+ }
63
+
64
+ /**
65
+ * Creates a Cartesian product of arrays.
66
+ * @param {Array<Array>} arrays - Array of arrays for Cartesian product.
67
+ * @returns {Array<Array>} Cartesian product.
68
+ */
69
+ function cartesianProduct(arrays) {
70
+ return arrays.reduce(
71
+ (acc, curr) =>
72
+ acc
73
+ .map((a) => curr.map((b) => a.concat([b])))
74
+ .reduce((a, b) => a.concat(b), []),
75
+ [[]],
76
+ );
77
+ }
78
+
79
+ /**
80
+ * Checks if an element is visible and interactable.
81
+ * Enhanced version inspired by getLocalHintsForElement.
82
+ * @param {Element} element - DOM element.
83
+ * @returns {boolean} True if the element is clickable and visible.
84
+ */
85
+ function isElementClickable(element) {
86
+ if (!(element instanceof Element)) return false;
87
+
88
+ const tagName = element.tagName.toLowerCase();
89
+ let isClickable = false;
90
+
91
+ // Check for aria-disabled
92
+ const ariaDisabled = element.getAttribute("aria-disabled");
93
+ if (ariaDisabled && ["", "true"].includes(ariaDisabled.toLowerCase())) {
94
+ return false; // Element should not be clickable if aria-disabled is true
95
+ }
96
+
97
+ // Check for visibility
98
+ const style = window.getComputedStyle(element);
99
+ if (
100
+ style.display === "none" ||
101
+ style.visibility === "hidden" ||
102
+ parseFloat(style.opacity) === 0 ||
103
+ style.pointerEvents === "none"
104
+ ) {
105
+ return false;
106
+ }
107
+
108
+ // Check if element is disabled (for applicable elements)
109
+ if (element.disabled) return false;
110
+
111
+ // Check for AngularJS click handlers
112
+ if (!isElementClickable._checkForAngularJs) {
113
+ isElementClickable._checkForAngularJs = (function () {
114
+ const angularElements = document.getElementsByClassName("ng-scope");
115
+ if (angularElements.length === 0) {
116
+ return () => false;
117
+ } else {
118
+ const ngAttributes = [];
119
+ for (const prefix of ["", "data-", "x-"]) {
120
+ for (const separator of ["-", ":", "_"]) {
121
+ ngAttributes.push(`${prefix}ng${separator}click`);
122
+ }
123
+ }
124
+ return function (el) {
125
+ for (const attribute of ngAttributes) {
126
+ if (el.hasAttribute(attribute)) return true;
127
+ }
128
+ return false;
129
+ };
130
+ }
131
+ })();
132
+ }
133
+
134
+ if (!isClickable && isElementClickable._checkForAngularJs(element)) {
135
+ isClickable = true;
136
+ }
137
+
138
+ // Check for onclick attribute or listener
139
+ if (
140
+ element.hasAttribute("onclick") ||
141
+ typeof element.onclick === "function"
142
+ ) {
143
+ isClickable = true;
144
+ }
145
+
146
+ // Check for jsaction attribute (commonly used in frameworks like Google's)
147
+ if (!isClickable && element.hasAttribute("jsaction")) {
148
+ const jsactionRules = element.getAttribute("jsaction").split(";");
149
+ for (const jsactionRule of jsactionRules) {
150
+ const ruleSplit = jsactionRule.trim().split(":");
151
+ if (ruleSplit.length >= 1 && ruleSplit.length <= 2) {
152
+ const [eventType] = ruleSplit[0].trim().split(".");
153
+ if (eventType === "click") {
154
+ isClickable = true;
155
+ break;
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ // Check for role attributes that imply clickability
162
+ if (!isClickable) {
163
+ const role = element.getAttribute("role");
164
+ const clickableRoles = [
165
+ "button",
166
+ "tab",
167
+ "link",
168
+ "checkbox",
169
+ "menuitem",
170
+ "menuitemcheckbox",
171
+ "menuitemradio",
172
+ "radio",
173
+ "switch",
174
+ ];
175
+ if (role && clickableRoles.includes(role.toLowerCase())) {
176
+ isClickable = true;
177
+ }
178
+ }
179
+
180
+ // Check for contentEditable
181
+ if (!isClickable) {
182
+ const contentEditable = element.getAttribute("contentEditable");
183
+ if (
184
+ contentEditable != null &&
185
+ ["", "contenteditable", "true"].includes(contentEditable.toLowerCase())
186
+ ) {
187
+ isClickable = true;
188
+ }
189
+ }
190
+
191
+ // Tag-specific clickability
192
+ const focusableTags = [
193
+ "a",
194
+ "button",
195
+ "input",
196
+ "select",
197
+ "textarea",
198
+ "object",
199
+ "embed",
200
+ "label",
201
+ "details",
202
+ ];
203
+ if (focusableTags.includes(tagName)) {
204
+ switch (tagName) {
205
+ case "a":
206
+ // Ensure it's not just a named anchor without href
207
+ if (element.hasAttribute("href")) {
208
+ isClickable = true;
209
+ }
210
+ break;
211
+ case "input": {
212
+ const type = (element.getAttribute("type") || "").toLowerCase();
213
+ if (
214
+ type !== "hidden" &&
215
+ !element.disabled &&
216
+ !(element.readOnly && isInputSelectable(element))
217
+ ) {
218
+ isClickable = true;
219
+ }
220
+ break;
221
+ }
222
+ case "textarea":
223
+ if (!element.disabled && !element.readOnly) {
224
+ isClickable = true;
225
+ }
226
+ break;
227
+ case "button":
228
+ case "select":
229
+ if (!element.disabled) {
230
+ isClickable = true;
231
+ }
232
+ break;
233
+ case "object":
234
+ case "embed":
235
+ isClickable = true;
236
+ break;
237
+ case "label":
238
+ if (element.control && !element.control.disabled) {
239
+ isClickable = true;
240
+ }
241
+ break;
242
+ case "details":
243
+ isClickable = true;
244
+ break;
245
+ default:
246
+ break;
247
+ }
248
+ }
249
+
250
+ // Check for class names containing 'button' as a possible click target
251
+ if (!isClickable) {
252
+ const className = element.getAttribute("class");
253
+ if (className && className.toLowerCase().includes("button")) {
254
+ isClickable = true;
255
+ } else if (element.classList.contains("cursor-pointer")) {
256
+ isClickable = true;
257
+ } else if (element.style.cursor === "pointer") {
258
+ isClickable = true;
259
+ }
260
+ }
261
+
262
+ // Check for tabindex
263
+ if (!isClickable) {
264
+ const tabIndexValue = element.getAttribute("tabindex");
265
+ const tabIndex = tabIndexValue ? parseInt(tabIndexValue) : -1;
266
+ if (tabIndex >= 0 && !isNaN(tabIndex)) {
267
+ isClickable = true;
268
+ }
269
+ }
270
+
271
+ return isClickable;
272
+ }
273
+
274
+ /**
275
+ * Helper function to determine if an input is selectable (e.g., text, search).
276
+ * @param {Element} input - The input element.
277
+ * @returns {boolean} True if the input type is selectable.
278
+ */
279
+ function isInputSelectable(input) {
280
+ const selectableTypes = [
281
+ "text",
282
+ "search",
283
+ "password",
284
+ "url",
285
+ "email",
286
+ "number",
287
+ "tel",
288
+ ];
289
+ const type = (input.getAttribute("type") || "").toLowerCase();
290
+ return selectableTypes.includes(type);
291
+ }
292
+
293
+ /**
294
+ * Generates a unique CSS selector path for a given element.
295
+ * @param {Element} element - The DOM element.
296
+ * @returns {string} A unique CSS selector string.
297
+ */
298
+ function getUniqueSelector(element) {
299
+ if (element.id) {
300
+ return `#${CSS.escape(element.id)}`;
301
+ }
302
+
303
+ const parts = [];
304
+ while (element && element.nodeType === Node.ELEMENT_NODE) {
305
+ let part = element.nodeName.toLowerCase();
306
+
307
+ // Use classList instead of className to avoid errors
308
+ if (element.classList && element.classList.length > 0) {
309
+ const classes = Array.from(element.classList)
310
+ .filter((cls) => cls.length > 0)
311
+ .map((cls) => `.${CSS.escape(cls)}`)
312
+ .join("");
313
+ part += classes;
314
+ }
315
+
316
+ // Add nth-child if necessary
317
+ const parent = element.parentNode;
318
+ if (parent) {
319
+ const siblings = Array.from(parent.children).filter(
320
+ (sibling) => sibling.nodeName === element.nodeName,
321
+ );
322
+ if (siblings.length > 1) {
323
+ const index =
324
+ Array.prototype.indexOf.call(parent.children, element) + 1;
325
+ part += `:nth-child(${index})`;
326
+ }
327
+ }
328
+
329
+ parts.unshift(part);
330
+ element = element.parentElement;
331
+ }
332
+
333
+ return parts.join(" > ");
334
+ }
335
+
336
+ /**
337
+ * Creates a hint marker element positioned at the top-left of the target element.
338
+ * @param {Element} el - The clickable DOM element.
339
+ * @param {string} hint - The hint identifier.
340
+ * @param {string} nodePath - Unique CSS selector path for the element.
341
+ * @returns {HTMLElement} The created hint marker element.
342
+ */
343
+ function createHintMarker(el, hint) {
344
+ const rect = el.getBoundingClientRect();
345
+
346
+ const marker = document.createElement("div");
347
+ marker.textContent = hint;
348
+ marker.className = markerClass;
349
+
350
+ // Style the marker
351
+ Object.assign(marker.style, {
352
+ position: "absolute",
353
+ top: `${rect.top + window.scrollY}px`,
354
+ left: `${rect.left + window.scrollX}px`,
355
+ background:
356
+ "-webkit-gradient(linear, 0% 0%, 0% 100%, from(rgb(255, 247, 133)), to(rgb(255, 197, 66)))",
357
+ padding: "1px 3px 0px",
358
+ borderRadius: "3px",
359
+ border: "1px solid rgb(227, 190, 35)",
360
+ fontSize: "11px",
361
+ pointerEvents: "none",
362
+ zIndex: "10000",
363
+ whiteSpace: "nowrap",
364
+ overflow: "hidden",
365
+ boxShadow: "rgba(0, 0, 0, 0.3) 0px 3px 7px 0px",
366
+ letterSpacing: 0,
367
+ minHeight: 0,
368
+ lineHeight: "100%",
369
+ color: "rgb(48, 37, 5)",
370
+ fontFamily: "Helvetica, Arial, sans-serif",
371
+ fontWeight: "bold",
372
+ textShadow: "rgba(255, 255, 255, 0.6) 0px 1px 0px",
373
+ });
374
+
375
+ annotationsContainer.appendChild(marker);
376
+
377
+ return marker;
378
+ }
379
+
380
+ /**
381
+ * Clears all existing hint markers from the annotations container.
382
+ */
383
+ function clearAnnotations() {
384
+ if (annotationsContainer) {
385
+ annotationsContainer.remove();
386
+ annotationsContainer = null;
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Initializes the annotations by creating the container and adding markers.
392
+ */
393
+ function initializeAnnotations() {
394
+ clearAnnotations(); // Clear any existing annotations
395
+
396
+ annotationsContainer = document.createElement("div");
397
+ annotationsContainer.className = "annotations";
398
+ // Ensure the container covers the entire page
399
+ Object.assign(annotationsContainer.style, {
400
+ position: "absolute",
401
+ top: "0",
402
+ left: "0",
403
+ width: "100%",
404
+ height: "100%",
405
+ pointerEvents: "none", // Allow clicks to pass through
406
+ zIndex: "9999", // Ensure it's above other elements
407
+ });
408
+ document.body.appendChild(annotationsContainer);
409
+
410
+ const clickableElements = Array.from(document.querySelectorAll("*")).filter(
411
+ (el) => isElementClickable(el) && isElementVisibleToUser(el),
412
+ );
413
+ const hints = generateHintStrings(
414
+ hintCharacterSet,
415
+ Math.min(maxHints, clickableElements.length),
416
+ );
417
+
418
+ clickableElements.slice(0, maxHints).forEach((el, index) => {
419
+ const hint = hints[index];
420
+ const nodePath = getUniqueSelector(el);
421
+ const rect = el.getBoundingClientRect();
422
+
423
+ // Create the hint marker
424
+ createHintMarker(el, hint, nodePath);
425
+ el.style.boxShadow = `inset 0 0 0px 2px red`;
426
+
427
+ // Populate the annotations map
428
+ annotationsMap[hint] = {
429
+ node: el,
430
+ nodePath: nodePath,
431
+ rect: {
432
+ top: rect.top + window.scrollY,
433
+ left: rect.left + window.scrollX,
434
+ width: rect.width,
435
+ height: rect.height,
436
+ right: rect.right + window.scrollX,
437
+ bottom: rect.bottom + window.scrollY,
438
+ },
439
+ };
440
+ });
441
+ }
442
+
443
+ /**
444
+ * Enables the annotations by making the annotations container visible.
445
+ */
446
+ function enable() {
447
+ if (!annotationsContainer) {
448
+ initializeAnnotations();
449
+ } else {
450
+ annotationsContainer.style.display = "block";
451
+ }
452
+ }
453
+
454
+ /**
455
+ * removes all generated hints from DOM
456
+ */
457
+ function destroy() {
458
+ if (annotationsContainer) {
459
+ Object.values(annotationsMap).forEach((annotation) => {
460
+ annotation.node.style.boxShadow = "none";
461
+ });
462
+
463
+ annotationsContainer.parentNode.removeChild(annotationsContainer);
464
+ }
465
+ }
466
+
467
+ // Initialize annotations upon first run
468
+ enable();
469
+
470
+ // Return the desired object
471
+ return {
472
+ annotations: annotationsMap,
473
+ destroy,
474
+ };
475
+ };
@@ -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;IAmB1C,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,UAAU;CAUX"}
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"}
@@ -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,iBAwIA"}
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 lastThreeLinesOfCode = actions.getLastCodeLines(3);
126
- const areLastActionsRepeatitive = lastThreeLinesOfCode.length === 3 &&
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;AAI3D,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,iBAkBxD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,QA+BjD;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAM1E;AAWD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,oBAAoB,EACtC,gBAAgB,GAAE,MAAM,EAAU,GACjC,OAAO,CAAC,MAAM,CAAC,CA+CjB;AAED,wBAAsB,sBAAsB,CAAC,EAC3C,YAAiB,EACjB,IAAS,EACT,eAAoB,EACpB,gBAAqB,EACrB,UAAyC,GAC1C,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,8EASA;AAED,qBAAa,eAAe;IACd,OAAO,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM;IACrC,OAAO,CAAC,aAAa,CAAqB;YAE5B,mBAAmB;YAUnB,gBAAgB;IAsBjB,OAAO;IAuBb,SAAS;CAKjB"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/agent/browsing/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAK3D,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;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 scriptResps = await Promise.all([
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(scriptResps.map((s) => s.text()));
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;;;GAiRA"}
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"}
@@ -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: (0, session_1.getSessionDetails)().version,
111
+ version: session.version,
111
112
  metadata: {
112
- generationId: (0, session_1.getSessionDetails)().generationId,
113
- sessionId: (0, session_1.getSessionDetails)().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,CAgFA,CAAC"}
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,475 @@
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 an element is visible to the user.
26
+ * It considers not just the viewport but also the scrollable parent.
27
+ * @param {Element} element - The DOM element to check.
28
+ * @returns {boolean} True if the element is visible to the user.
29
+ */
30
+ function isElementVisibleToUser(element) {
31
+ const rect = element.getBoundingClientRect();
32
+ return (
33
+ rect.top >= 0 &&
34
+ rect.left >= 0 &&
35
+ rect.bottom <=
36
+ (window.innerHeight || document.documentElement.clientHeight) &&
37
+ rect.right <= (window.innerWidth || document.documentElement.clientWidth)
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Generates a sequence of hint strings based on the provided character set.
43
+ * For example, with 'abc' as the set and maxHints 5, it generates: 'a', 'b', 'c', 'aa', 'ab'
44
+ * @param {string} charset - Characters to use.
45
+ * @param {number} max - Maximum number of hints to generate.
46
+ * @returns {string[]} Array of hint strings.
47
+ */
48
+ function generateHintStrings(charset, max) {
49
+ const hints = [];
50
+ let length = 1;
51
+
52
+ while (hints.length < max) {
53
+ const combos = cartesianProduct(Array(length).fill(charset.split("")));
54
+ for (const combo of combos) {
55
+ hints.push(combo.join(""));
56
+ if (hints.length >= max) break;
57
+ }
58
+ length++;
59
+ }
60
+
61
+ return hints;
62
+ }
63
+
64
+ /**
65
+ * Creates a Cartesian product of arrays.
66
+ * @param {Array<Array>} arrays - Array of arrays for Cartesian product.
67
+ * @returns {Array<Array>} Cartesian product.
68
+ */
69
+ function cartesianProduct(arrays) {
70
+ return arrays.reduce(
71
+ (acc, curr) =>
72
+ acc
73
+ .map((a) => curr.map((b) => a.concat([b])))
74
+ .reduce((a, b) => a.concat(b), []),
75
+ [[]],
76
+ );
77
+ }
78
+
79
+ /**
80
+ * Checks if an element is visible and interactable.
81
+ * Enhanced version inspired by getLocalHintsForElement.
82
+ * @param {Element} element - DOM element.
83
+ * @returns {boolean} True if the element is clickable and visible.
84
+ */
85
+ function isElementClickable(element) {
86
+ if (!(element instanceof Element)) return false;
87
+
88
+ const tagName = element.tagName.toLowerCase();
89
+ let isClickable = false;
90
+
91
+ // Check for aria-disabled
92
+ const ariaDisabled = element.getAttribute("aria-disabled");
93
+ if (ariaDisabled && ["", "true"].includes(ariaDisabled.toLowerCase())) {
94
+ return false; // Element should not be clickable if aria-disabled is true
95
+ }
96
+
97
+ // Check for visibility
98
+ const style = window.getComputedStyle(element);
99
+ if (
100
+ style.display === "none" ||
101
+ style.visibility === "hidden" ||
102
+ parseFloat(style.opacity) === 0 ||
103
+ style.pointerEvents === "none"
104
+ ) {
105
+ return false;
106
+ }
107
+
108
+ // Check if element is disabled (for applicable elements)
109
+ if (element.disabled) return false;
110
+
111
+ // Check for AngularJS click handlers
112
+ if (!isElementClickable._checkForAngularJs) {
113
+ isElementClickable._checkForAngularJs = (function () {
114
+ const angularElements = document.getElementsByClassName("ng-scope");
115
+ if (angularElements.length === 0) {
116
+ return () => false;
117
+ } else {
118
+ const ngAttributes = [];
119
+ for (const prefix of ["", "data-", "x-"]) {
120
+ for (const separator of ["-", ":", "_"]) {
121
+ ngAttributes.push(`${prefix}ng${separator}click`);
122
+ }
123
+ }
124
+ return function (el) {
125
+ for (const attribute of ngAttributes) {
126
+ if (el.hasAttribute(attribute)) return true;
127
+ }
128
+ return false;
129
+ };
130
+ }
131
+ })();
132
+ }
133
+
134
+ if (!isClickable && isElementClickable._checkForAngularJs(element)) {
135
+ isClickable = true;
136
+ }
137
+
138
+ // Check for onclick attribute or listener
139
+ if (
140
+ element.hasAttribute("onclick") ||
141
+ typeof element.onclick === "function"
142
+ ) {
143
+ isClickable = true;
144
+ }
145
+
146
+ // Check for jsaction attribute (commonly used in frameworks like Google's)
147
+ if (!isClickable && element.hasAttribute("jsaction")) {
148
+ const jsactionRules = element.getAttribute("jsaction").split(";");
149
+ for (const jsactionRule of jsactionRules) {
150
+ const ruleSplit = jsactionRule.trim().split(":");
151
+ if (ruleSplit.length >= 1 && ruleSplit.length <= 2) {
152
+ const [eventType] = ruleSplit[0].trim().split(".");
153
+ if (eventType === "click") {
154
+ isClickable = true;
155
+ break;
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ // Check for role attributes that imply clickability
162
+ if (!isClickable) {
163
+ const role = element.getAttribute("role");
164
+ const clickableRoles = [
165
+ "button",
166
+ "tab",
167
+ "link",
168
+ "checkbox",
169
+ "menuitem",
170
+ "menuitemcheckbox",
171
+ "menuitemradio",
172
+ "radio",
173
+ "switch",
174
+ ];
175
+ if (role && clickableRoles.includes(role.toLowerCase())) {
176
+ isClickable = true;
177
+ }
178
+ }
179
+
180
+ // Check for contentEditable
181
+ if (!isClickable) {
182
+ const contentEditable = element.getAttribute("contentEditable");
183
+ if (
184
+ contentEditable != null &&
185
+ ["", "contenteditable", "true"].includes(contentEditable.toLowerCase())
186
+ ) {
187
+ isClickable = true;
188
+ }
189
+ }
190
+
191
+ // Tag-specific clickability
192
+ const focusableTags = [
193
+ "a",
194
+ "button",
195
+ "input",
196
+ "select",
197
+ "textarea",
198
+ "object",
199
+ "embed",
200
+ "label",
201
+ "details",
202
+ ];
203
+ if (focusableTags.includes(tagName)) {
204
+ switch (tagName) {
205
+ case "a":
206
+ // Ensure it's not just a named anchor without href
207
+ if (element.hasAttribute("href")) {
208
+ isClickable = true;
209
+ }
210
+ break;
211
+ case "input": {
212
+ const type = (element.getAttribute("type") || "").toLowerCase();
213
+ if (
214
+ type !== "hidden" &&
215
+ !element.disabled &&
216
+ !(element.readOnly && isInputSelectable(element))
217
+ ) {
218
+ isClickable = true;
219
+ }
220
+ break;
221
+ }
222
+ case "textarea":
223
+ if (!element.disabled && !element.readOnly) {
224
+ isClickable = true;
225
+ }
226
+ break;
227
+ case "button":
228
+ case "select":
229
+ if (!element.disabled) {
230
+ isClickable = true;
231
+ }
232
+ break;
233
+ case "object":
234
+ case "embed":
235
+ isClickable = true;
236
+ break;
237
+ case "label":
238
+ if (element.control && !element.control.disabled) {
239
+ isClickable = true;
240
+ }
241
+ break;
242
+ case "details":
243
+ isClickable = true;
244
+ break;
245
+ default:
246
+ break;
247
+ }
248
+ }
249
+
250
+ // Check for class names containing 'button' as a possible click target
251
+ if (!isClickable) {
252
+ const className = element.getAttribute("class");
253
+ if (className && className.toLowerCase().includes("button")) {
254
+ isClickable = true;
255
+ } else if (element.classList.contains("cursor-pointer")) {
256
+ isClickable = true;
257
+ } else if (element.style.cursor === "pointer") {
258
+ isClickable = true;
259
+ }
260
+ }
261
+
262
+ // Check for tabindex
263
+ if (!isClickable) {
264
+ const tabIndexValue = element.getAttribute("tabindex");
265
+ const tabIndex = tabIndexValue ? parseInt(tabIndexValue) : -1;
266
+ if (tabIndex >= 0 && !isNaN(tabIndex)) {
267
+ isClickable = true;
268
+ }
269
+ }
270
+
271
+ return isClickable;
272
+ }
273
+
274
+ /**
275
+ * Helper function to determine if an input is selectable (e.g., text, search).
276
+ * @param {Element} input - The input element.
277
+ * @returns {boolean} True if the input type is selectable.
278
+ */
279
+ function isInputSelectable(input) {
280
+ const selectableTypes = [
281
+ "text",
282
+ "search",
283
+ "password",
284
+ "url",
285
+ "email",
286
+ "number",
287
+ "tel",
288
+ ];
289
+ const type = (input.getAttribute("type") || "").toLowerCase();
290
+ return selectableTypes.includes(type);
291
+ }
292
+
293
+ /**
294
+ * Generates a unique CSS selector path for a given element.
295
+ * @param {Element} element - The DOM element.
296
+ * @returns {string} A unique CSS selector string.
297
+ */
298
+ function getUniqueSelector(element) {
299
+ if (element.id) {
300
+ return `#${CSS.escape(element.id)}`;
301
+ }
302
+
303
+ const parts = [];
304
+ while (element && element.nodeType === Node.ELEMENT_NODE) {
305
+ let part = element.nodeName.toLowerCase();
306
+
307
+ // Use classList instead of className to avoid errors
308
+ if (element.classList && element.classList.length > 0) {
309
+ const classes = Array.from(element.classList)
310
+ .filter((cls) => cls.length > 0)
311
+ .map((cls) => `.${CSS.escape(cls)}`)
312
+ .join("");
313
+ part += classes;
314
+ }
315
+
316
+ // Add nth-child if necessary
317
+ const parent = element.parentNode;
318
+ if (parent) {
319
+ const siblings = Array.from(parent.children).filter(
320
+ (sibling) => sibling.nodeName === element.nodeName,
321
+ );
322
+ if (siblings.length > 1) {
323
+ const index =
324
+ Array.prototype.indexOf.call(parent.children, element) + 1;
325
+ part += `:nth-child(${index})`;
326
+ }
327
+ }
328
+
329
+ parts.unshift(part);
330
+ element = element.parentElement;
331
+ }
332
+
333
+ return parts.join(" > ");
334
+ }
335
+
336
+ /**
337
+ * Creates a hint marker element positioned at the top-left of the target element.
338
+ * @param {Element} el - The clickable DOM element.
339
+ * @param {string} hint - The hint identifier.
340
+ * @param {string} nodePath - Unique CSS selector path for the element.
341
+ * @returns {HTMLElement} The created hint marker element.
342
+ */
343
+ function createHintMarker(el, hint) {
344
+ const rect = el.getBoundingClientRect();
345
+
346
+ const marker = document.createElement("div");
347
+ marker.textContent = hint;
348
+ marker.className = markerClass;
349
+
350
+ // Style the marker
351
+ Object.assign(marker.style, {
352
+ position: "absolute",
353
+ top: `${rect.top + window.scrollY}px`,
354
+ left: `${rect.left + window.scrollX}px`,
355
+ background:
356
+ "-webkit-gradient(linear, 0% 0%, 0% 100%, from(rgb(255, 247, 133)), to(rgb(255, 197, 66)))",
357
+ padding: "1px 3px 0px",
358
+ borderRadius: "3px",
359
+ border: "1px solid rgb(227, 190, 35)",
360
+ fontSize: "11px",
361
+ pointerEvents: "none",
362
+ zIndex: "10000",
363
+ whiteSpace: "nowrap",
364
+ overflow: "hidden",
365
+ boxShadow: "rgba(0, 0, 0, 0.3) 0px 3px 7px 0px",
366
+ letterSpacing: 0,
367
+ minHeight: 0,
368
+ lineHeight: "100%",
369
+ color: "rgb(48, 37, 5)",
370
+ fontFamily: "Helvetica, Arial, sans-serif",
371
+ fontWeight: "bold",
372
+ textShadow: "rgba(255, 255, 255, 0.6) 0px 1px 0px",
373
+ });
374
+
375
+ annotationsContainer.appendChild(marker);
376
+
377
+ return marker;
378
+ }
379
+
380
+ /**
381
+ * Clears all existing hint markers from the annotations container.
382
+ */
383
+ function clearAnnotations() {
384
+ if (annotationsContainer) {
385
+ annotationsContainer.remove();
386
+ annotationsContainer = null;
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Initializes the annotations by creating the container and adding markers.
392
+ */
393
+ function initializeAnnotations() {
394
+ clearAnnotations(); // Clear any existing annotations
395
+
396
+ annotationsContainer = document.createElement("div");
397
+ annotationsContainer.className = "annotations";
398
+ // Ensure the container covers the entire page
399
+ Object.assign(annotationsContainer.style, {
400
+ position: "absolute",
401
+ top: "0",
402
+ left: "0",
403
+ width: "100%",
404
+ height: "100%",
405
+ pointerEvents: "none", // Allow clicks to pass through
406
+ zIndex: "9999", // Ensure it's above other elements
407
+ });
408
+ document.body.appendChild(annotationsContainer);
409
+
410
+ const clickableElements = Array.from(document.querySelectorAll("*")).filter(
411
+ (el) => isElementClickable(el) && isElementVisibleToUser(el),
412
+ );
413
+ const hints = generateHintStrings(
414
+ hintCharacterSet,
415
+ Math.min(maxHints, clickableElements.length),
416
+ );
417
+
418
+ clickableElements.slice(0, maxHints).forEach((el, index) => {
419
+ const hint = hints[index];
420
+ const nodePath = getUniqueSelector(el);
421
+ const rect = el.getBoundingClientRect();
422
+
423
+ // Create the hint marker
424
+ createHintMarker(el, hint, nodePath);
425
+ el.style.boxShadow = `inset 0 0 0px 2px red`;
426
+
427
+ // Populate the annotations map
428
+ annotationsMap[hint] = {
429
+ node: el,
430
+ nodePath: nodePath,
431
+ rect: {
432
+ top: rect.top + window.scrollY,
433
+ left: rect.left + window.scrollX,
434
+ width: rect.width,
435
+ height: rect.height,
436
+ right: rect.right + window.scrollX,
437
+ bottom: rect.bottom + window.scrollY,
438
+ },
439
+ };
440
+ });
441
+ }
442
+
443
+ /**
444
+ * Enables the annotations by making the annotations container visible.
445
+ */
446
+ function enable() {
447
+ if (!annotationsContainer) {
448
+ initializeAnnotations();
449
+ } else {
450
+ annotationsContainer.style.display = "block";
451
+ }
452
+ }
453
+
454
+ /**
455
+ * removes all generated hints from DOM
456
+ */
457
+ function destroy() {
458
+ if (annotationsContainer) {
459
+ Object.values(annotationsMap).forEach((annotation) => {
460
+ annotation.node.style.boxShadow = "none";
461
+ });
462
+
463
+ annotationsContainer.parentNode.removeChild(annotationsContainer);
464
+ }
465
+ }
466
+
467
+ // Initialize annotations upon first run
468
+ enable();
469
+
470
+ // Return the desired object
471
+ return {
472
+ annotations: annotationsMap,
473
+ destroy,
474
+ };
475
+ };
@@ -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,iBAoCxD"}
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)({
@@ -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":"AAgBA,iBAAS,iBAAiB;;;;EAMzB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,YAAY,GACb,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB,QAGA;AAED,wBAAsB,iBAAiB,qBAGtC;AAED,wBAAsB,eAAe,0DAqBpC;AAED,wBAAsB,UAAU,kBAqB/B;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
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"}
@@ -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.1",
3
+ "version": "0.35.3",
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",