@dev-blinq/cucumber_client 1.0.1457-dev → 1.0.1457-stage

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.
Files changed (34) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +73 -73
  2. package/bin/assets/scripts/recorder.js +87 -49
  3. package/bin/assets/scripts/snapshot_capturer.js +10 -17
  4. package/bin/assets/scripts/unique_locators.js +169 -47
  5. package/bin/assets/templates/_hooks_template.txt +6 -2
  6. package/bin/assets/templates/utils_template.txt +16 -16
  7. package/bin/client/code_cleanup/utils.js +16 -7
  8. package/bin/client/code_gen/code_inversion.js +115 -0
  9. package/bin/client/code_gen/duplication_analysis.js +2 -1
  10. package/bin/client/code_gen/function_signature.js +4 -0
  11. package/bin/client/code_gen/page_reflection.js +92 -11
  12. package/bin/client/code_gen/playwright_codeget.js +165 -76
  13. package/bin/client/cucumber/feature.js +4 -17
  14. package/bin/client/cucumber/steps_definitions.js +13 -0
  15. package/bin/client/local_agent.js +1 -0
  16. package/bin/client/recorderv3/bvt_init.js +320 -0
  17. package/bin/client/recorderv3/bvt_recorder.js +1312 -63
  18. package/bin/client/recorderv3/implemented_steps.js +2 -0
  19. package/bin/client/recorderv3/index.js +3 -293
  20. package/bin/client/recorderv3/services.js +819 -142
  21. package/bin/client/recorderv3/step_runner.js +35 -6
  22. package/bin/client/recorderv3/step_utils.js +175 -95
  23. package/bin/client/recorderv3/update_feature.js +87 -39
  24. package/bin/client/recorderv3/wbr_entry.js +61 -0
  25. package/bin/client/recording.js +1 -0
  26. package/bin/client/upload-service.js +2 -0
  27. package/bin/client/utils/app_dir.js +21 -0
  28. package/bin/client/utils/socket_logger.js +87 -125
  29. package/bin/index.js +4 -1
  30. package/package.json +11 -5
  31. package/bin/client/recorderv3/app_dir.js +0 -23
  32. package/bin/client/recorderv3/network.js +0 -299
  33. package/bin/client/recorderv3/scriptTest.js +0 -5
  34. package/bin/client/recorderv3/ws_server.js +0 -72
@@ -1,24 +1,345 @@
1
1
  // define the jsdoc type for the input
2
2
  import { closeContext, initContext, _getDataFile, resetTestData } from "automation_model";
3
- import { existsSync, readdirSync, readFileSync, rmSync } from "fs";
4
- import path from "path";
5
- import url from "url";
3
+ import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
4
+ import { rm } from "fs/promises";
5
+ import path from "node:path";
6
+ import url from "node:url";
6
7
  import { getImplementedSteps, parseRouteFiles } from "./implemented_steps.js";
7
- import { NamesService } from "./services.js";
8
+ import { NamesService, PublishService } from "./services.js";
8
9
  import { BVTStepRunner } from "./step_runner.js";
9
- import { readFile, writeFile } from "fs/promises";
10
- import { updateStepDefinitions, loadStepDefinitions, getCommandsForImplementedStep } from "./step_utils.js";
11
- import { updateFeatureFile } from "./update_feature.js";
10
+ import { readFile, writeFile } from "node:fs/promises";
11
+ import {
12
+ loadStepDefinitions,
13
+ getCommandsForImplementedStep,
14
+ updateStepDefinitions,
15
+ getCodePage,
16
+ getCucumberStep,
17
+ _toRecordingStep,
18
+ toMethodName,
19
+ } from "./step_utils.js";
12
20
  import { parseStepTextParameters } from "../cucumber/utils.js";
13
21
  import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
14
22
  import chokidar from "chokidar";
15
23
  import { unEscapeNonPrintables } from "../cucumber/utils.js";
16
- import { findAvailablePort } from "../utils/index.js";
17
- import socketLogger from "../utils/socket_logger.js";
24
+ import { findAvailablePort, getRunsServiceBaseURL } from "../utils/index.js";
25
+ import socketLogger, { getErrorMessage } from "../utils/socket_logger.js";
18
26
  import { tmpdir } from "os";
27
+ import { faker } from "@faker-js/faker/locale/en_US";
28
+ import { chromium } from "playwright-core";
29
+ import { axiosClient } from "../utils/axiosClient.js";
30
+ import { _generateCodeFromCommand } from "../code_gen/playwright_codeget.js";
31
+ import { Recording } from "../recording.js";
32
+
19
33
  const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
20
34
 
21
35
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
36
+
37
+ const clipboardBridgeScript = `
38
+ ;(() => {
39
+ if (window.__bvtRecorderClipboardBridgeInitialized) {
40
+ console.log('[ClipboardBridge] Already initialized, skipping');
41
+ return;
42
+ }
43
+ window.__bvtRecorderClipboardBridgeInitialized = true;
44
+ console.log('[ClipboardBridge] Initializing clipboard bridge');
45
+
46
+ const emitPayload = (payload, attempt = 0) => {
47
+ const reporter = window.__bvt_reportClipboard;
48
+ if (typeof reporter === "function") {
49
+ try {
50
+ console.log('[ClipboardBridge] Reporting clipboard payload:', payload);
51
+ reporter(payload);
52
+ } catch (error) {
53
+ console.warn("[ClipboardBridge] Failed to report payload", error);
54
+ }
55
+ return;
56
+ }
57
+ if (attempt < 5) {
58
+ console.log('[ClipboardBridge] Reporter not ready, retrying...', attempt);
59
+ setTimeout(() => emitPayload(payload, attempt + 1), 50 * (attempt + 1));
60
+ } else {
61
+ console.warn('[ClipboardBridge] Reporter never became available');
62
+ }
63
+ };
64
+
65
+ const fileToBase64 = (file) => {
66
+ return new Promise((resolve) => {
67
+ try {
68
+ const reader = new FileReader();
69
+ reader.onload = () => {
70
+ const { result } = reader;
71
+ if (typeof result === "string") {
72
+ const index = result.indexOf("base64,");
73
+ resolve(index !== -1 ? result.substring(index + 7) : result);
74
+ return;
75
+ }
76
+ if (result instanceof ArrayBuffer) {
77
+ const bytes = new Uint8Array(result);
78
+ let binary = "";
79
+ const chunk = 0x8000;
80
+ for (let i = 0; i < bytes.length; i += chunk) {
81
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
82
+ }
83
+ resolve(btoa(binary));
84
+ return;
85
+ }
86
+ resolve(null);
87
+ };
88
+ reader.onerror = () => resolve(null);
89
+ reader.readAsDataURL(file);
90
+ } catch (error) {
91
+ console.warn("[ClipboardBridge] Failed to serialize file", error);
92
+ resolve(null);
93
+ }
94
+ });
95
+ };
96
+
97
+ const handleClipboardEvent = async (event) => {
98
+ try {
99
+ console.log('[ClipboardBridge] Handling clipboard event:', event.type);
100
+ const payload = { trigger: event.type };
101
+ const clipboardData = event.clipboardData;
102
+
103
+ if (clipboardData) {
104
+ try {
105
+ const text = clipboardData.getData("text/plain");
106
+ if (text) {
107
+ payload.text = text;
108
+ console.log('[ClipboardBridge] Captured text:', text.substring(0, 50));
109
+ }
110
+ } catch (error) {
111
+ console.warn("[ClipboardBridge] Could not read text/plain", error);
112
+ }
113
+
114
+ try {
115
+ const html = clipboardData.getData("text/html");
116
+ if (html) {
117
+ payload.html = html;
118
+ console.log('[ClipboardBridge] Captured HTML:', html.substring(0, 50));
119
+ }
120
+ } catch (error) {
121
+ console.warn("[ClipboardBridge] Could not read text/html", error);
122
+ }
123
+
124
+ const files = clipboardData.files;
125
+ if (files && files.length > 0) {
126
+ console.log('[ClipboardBridge] Processing files:', files.length);
127
+ const serialized = [];
128
+ for (const file of files) {
129
+ const data = await fileToBase64(file);
130
+ if (data) {
131
+ serialized.push({
132
+ name: file.name,
133
+ type: file.type,
134
+ lastModified: file.lastModified,
135
+ data,
136
+ });
137
+ }
138
+ }
139
+ if (serialized.length > 0) {
140
+ payload.files = serialized;
141
+ }
142
+ }
143
+ }
144
+
145
+ if (!payload.text) {
146
+ try {
147
+ const selection = window.getSelection?.();
148
+ const selectionText = selection?.toString?.();
149
+ if (selectionText) {
150
+ payload.text = selectionText;
151
+ console.log('[ClipboardBridge] Using selection text:', selectionText.substring(0, 50));
152
+ }
153
+ } catch {
154
+ // Ignore selection access errors.
155
+ }
156
+ }
157
+
158
+ emitPayload(payload);
159
+ } catch (error) {
160
+ console.warn("[ClipboardBridge] Could not process event", error);
161
+ }
162
+ };
163
+
164
+ // NEW: Function to apply clipboard data to the page
165
+ window.__bvt_applyClipboardData = (payload) => {
166
+ console.log('[ClipboardBridge] Applying clipboard data:', payload);
167
+
168
+ if (!payload) {
169
+ console.warn('[ClipboardBridge] No payload provided');
170
+ return false;
171
+ }
172
+
173
+ try {
174
+ // Create DataTransfer object
175
+ let dataTransfer = null;
176
+ try {
177
+ dataTransfer = new DataTransfer();
178
+ console.log('[ClipboardBridge] DataTransfer created');
179
+ } catch (error) {
180
+ console.warn('[ClipboardBridge] Could not create DataTransfer', error);
181
+ }
182
+
183
+ if (dataTransfer) {
184
+ if (payload.text) {
185
+ try {
186
+ dataTransfer.setData("text/plain", payload.text);
187
+ console.log('[ClipboardBridge] Set text/plain:', payload.text.substring(0, 50));
188
+ } catch (error) {
189
+ console.warn('[ClipboardBridge] Failed to set text/plain', error);
190
+ }
191
+ }
192
+ if (payload.html) {
193
+ try {
194
+ dataTransfer.setData("text/html", payload.html);
195
+ console.log('[ClipboardBridge] Set text/html:', payload.html.substring(0, 50));
196
+ } catch (error) {
197
+ console.warn('[ClipboardBridge] Failed to set text/html', error);
198
+ }
199
+ }
200
+ }
201
+
202
+ // Get target element
203
+ let target = document.activeElement || document.body;
204
+ console.log('[ClipboardBridge] Target element:', {
205
+ tagName: target.tagName,
206
+ type: target.type,
207
+ isContentEditable: target.isContentEditable,
208
+ id: target.id,
209
+ className: target.className
210
+ });
211
+
212
+ // Try synthetic paste event first
213
+ let pasteHandled = false;
214
+ if (dataTransfer && target && typeof target.dispatchEvent === "function") {
215
+ try {
216
+ const pasteEvent = new ClipboardEvent("paste", {
217
+ clipboardData: dataTransfer,
218
+ bubbles: true,
219
+ cancelable: true,
220
+ });
221
+ pasteHandled = target.dispatchEvent(pasteEvent);
222
+ console.log('[ClipboardBridge] Paste event dispatched, handled:', pasteHandled);
223
+ } catch (error) {
224
+ console.warn('[ClipboardBridge] Failed to dispatch paste event', error);
225
+ }
226
+ }
227
+
228
+
229
+ console.log('[ClipboardBridge] Paste event not handled, trying fallback methods');
230
+
231
+ // Fallback: Try execCommand with HTML first (for contenteditable)
232
+ if (payload.html && target.isContentEditable) {
233
+ console.log('[ClipboardBridge] Trying execCommand insertHTML');
234
+ try {
235
+ const inserted = document.execCommand('insertHTML', false, payload.html);
236
+ if (inserted) {
237
+ console.log('[ClipboardBridge] Successfully inserted HTML via execCommand');
238
+ return true;
239
+ }
240
+ } catch (error) {
241
+ console.warn('[ClipboardBridge] execCommand insertHTML failed', error);
242
+ }
243
+
244
+ // Try Range API for HTML
245
+ console.log('[ClipboardBridge] Trying Range API for HTML');
246
+ try {
247
+ const selection = window.getSelection?.();
248
+ if (selection && selection.rangeCount > 0) {
249
+ const range = selection.getRangeAt(0);
250
+ range.deleteContents();
251
+ const fragment = range.createContextualFragment(payload.html);
252
+ range.insertNode(fragment);
253
+ range.collapse(false);
254
+ console.log('[ClipboardBridge] Successfully inserted HTML via Range API');
255
+ return true;
256
+ }
257
+ } catch (error) {
258
+ console.warn('[ClipboardBridge] Range API HTML insertion failed', error);
259
+ }
260
+ }
261
+
262
+ // Fallback: Try execCommand with text
263
+ if (payload.text) {
264
+ console.log('[ClipboardBridge] Trying execCommand insertText');
265
+ try {
266
+ const inserted = document.execCommand('insertText', false, payload.text);
267
+ if (inserted) {
268
+ console.log('[ClipboardBridge] Successfully inserted text via execCommand');
269
+ return true;
270
+ }
271
+ } catch (error) {
272
+ console.warn('[ClipboardBridge] execCommand insertText failed', error);
273
+ }
274
+
275
+ // Try Range API for text
276
+ if (target.isContentEditable) {
277
+ console.log('[ClipboardBridge] Trying Range API for text');
278
+ try {
279
+ const selection = window.getSelection?.();
280
+ if (selection && selection.rangeCount > 0) {
281
+ const range = selection.getRangeAt(0);
282
+ range.deleteContents();
283
+ range.insertNode(document.createTextNode(payload.text));
284
+ range.collapse(false);
285
+ console.log('[ClipboardBridge] Successfully inserted text via Range API');
286
+ return true;
287
+ }
288
+ } catch (error) {
289
+ console.warn('[ClipboardBridge] Range API text insertion failed', error);
290
+ }
291
+ }
292
+
293
+ // Last resort: Direct value assignment for input/textarea
294
+ if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
295
+ console.log('[ClipboardBridge] Trying direct value assignment');
296
+ try {
297
+ const start = target.selectionStart ?? target.value.length ?? 0;
298
+ const end = target.selectionEnd ?? target.value.length ?? 0;
299
+ const value = target.value ?? "";
300
+ const text = payload.text;
301
+ target.value = value.slice(0, start) + text + value.slice(end);
302
+ const caret = start + text.length;
303
+ if (typeof target.setSelectionRange === 'function') {
304
+ target.setSelectionRange(caret, caret);
305
+ }
306
+ target.dispatchEvent(new Event('input', { bubbles: true }));
307
+ console.log('[ClipboardBridge] Successfully set value directly');
308
+ return true;
309
+ } catch (error) {
310
+ console.warn('[ClipboardBridge] Direct value assignment failed', error);
311
+ }
312
+ }
313
+ }
314
+
315
+ console.warn('[ClipboardBridge] All paste methods failed');
316
+ return false;
317
+ } catch (error) {
318
+ console.error('[ClipboardBridge] Error applying clipboard data:', error);
319
+ return false;
320
+ }
321
+ };
322
+
323
+ // Set up event listeners for copy/cut
324
+ document.addEventListener(
325
+ "copy",
326
+ (event) => {
327
+ void handleClipboardEvent(event);
328
+ },
329
+ true
330
+ );
331
+ document.addEventListener(
332
+ "cut",
333
+ (event) => {
334
+ void handleClipboardEvent(event);
335
+ },
336
+ true
337
+ );
338
+
339
+ console.log('[ClipboardBridge] Clipboard bridge initialized successfully');
340
+ })();
341
+ `;
342
+
22
343
  export function getInitScript(config, options) {
23
344
  const preScript = `
24
345
  window.__bvt_Recorder_config = ${JSON.stringify(config ?? null)};
@@ -28,7 +349,7 @@ export function getInitScript(config, options) {
28
349
  path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"),
29
350
  "utf8"
30
351
  );
31
- return preScript + recorderScript;
352
+ return preScript + recorderScript + clipboardBridgeScript;
32
353
  }
33
354
 
34
355
  async function evaluate(frame, script) {
@@ -42,7 +363,6 @@ async function evaluate(frame, script) {
42
363
  }
43
364
 
44
365
  async function findNestedFrameSelector(frame, obj) {
45
- console.log("Testing new version");
46
366
  try {
47
367
  const parent = frame.parentFrame();
48
368
  if (!parent) return { children: obj };
@@ -53,8 +373,7 @@ async function findNestedFrameSelector(frame, obj) {
53
373
  }, frameElement);
54
374
  return findNestedFrameSelector(parent, { children: obj, selectors });
55
375
  } catch (e) {
56
- socketLogger.error(`Error in findNestedFrameSelector: ${e}`);
57
- console.error(e);
376
+ socketLogger.error(`Error in script evaluation: ${getErrorMessage(e)}`, undefined, "findNestedFrameSelector");
58
377
  }
59
378
  }
60
379
  const transformFillAction = (action, el) => {
@@ -156,6 +475,17 @@ const transformAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode,
156
475
  }
157
476
  }
158
477
  };
478
+ const diffPaths = (currentPath, newPath) => {
479
+ const currentDomain = new URL(currentPath).hostname;
480
+ const newDomain = new URL(newPath).hostname;
481
+ if (currentDomain !== newDomain) {
482
+ return true;
483
+ } else {
484
+ const currentRoute = new URL(currentPath).pathname;
485
+ const newRoute = new URL(newPath).pathname;
486
+ return currentRoute !== newRoute;
487
+ }
488
+ };
159
489
  /**
160
490
  * @typedef {Object} BVTRecorderInput
161
491
  * @property {string} envName
@@ -185,12 +515,16 @@ export class BVTRecorder {
185
515
  projectDir: this.projectDir,
186
516
  logger: this.logger,
187
517
  });
518
+ this.workspaceService = new PublishService(this.TOKEN);
188
519
  this.pageSet = new Set();
189
520
  this.lastKnownUrlPath = "";
190
- this.world = { attach: () => {} };
521
+ this.world = { attach: () => { } };
191
522
  this.shouldTakeScreenshot = true;
192
523
  this.watcher = null;
193
524
  this.networkEventsFolder = path.join(tmpdir(), "blinq_network_events");
525
+ this.tempProjectFolder = `${tmpdir()}/bvt_temp_project_${Math.floor(Math.random() * 1000000)}`;
526
+ this.tempSnapshotsFolder = path.join(this.tempProjectFolder, "data/snapshots");
527
+
194
528
  if (existsSync(this.networkEventsFolder)) {
195
529
  rmSync(this.networkEventsFolder, { recursive: true, force: true });
196
530
  }
@@ -208,6 +542,12 @@ export class BVTRecorder {
208
542
  cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
209
543
  cmdExecutionError: "BVTRecorder.cmdExecutionError",
210
544
  interceptResults: "BVTRecorder.interceptResults",
545
+ onDebugURLChange: "BVTRecorder.onDebugURLChange",
546
+ updateCommand: "BVTRecorder.updateCommand",
547
+ browserStateSync: "BrowserService.stateSync",
548
+ browserStateError: "BrowserService.stateError",
549
+ clipboardPush: "BrowserService.clipboardPush",
550
+ clipboardError: "BrowserService.clipboardError",
211
551
  };
212
552
  bindings = {
213
553
  __bvt_recordCommand: async ({ frame, page, context }, event) => {
@@ -237,6 +577,25 @@ export class BVTRecorder {
237
577
  __bvt_getObject: (_src, obj) => {
238
578
  this.processObject(obj);
239
579
  },
580
+ __bvt_reportClipboard: async ({ page }, payload) => {
581
+ try {
582
+ if (!payload) {
583
+ return;
584
+ }
585
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
586
+ if (activePage && activePage !== page) {
587
+ return;
588
+ }
589
+ const pageUrl = typeof page?.url === "function" ? page.url() : null;
590
+ this.sendEvent(this.events.clipboardPush, {
591
+ data: payload,
592
+ trigger: payload?.trigger ?? "copy",
593
+ pageUrl,
594
+ });
595
+ } catch (error) {
596
+ this.logger.error("Error forwarding clipboard payload from page", error);
597
+ }
598
+ },
240
599
  };
241
600
 
242
601
  getSnapshot = async (attr) => {
@@ -294,11 +653,15 @@ export class BVTRecorder {
294
653
  }
295
654
 
296
655
  async _initBrowser({ url }) {
297
- this.#remoteDebuggerPort = await findAvailablePort();
298
- process.env.CDP_LISTEN_PORT = this.#remoteDebuggerPort;
656
+ if (process.env.CDP_LISTEN_PORT === undefined) {
657
+ this.#remoteDebuggerPort = await findAvailablePort();
658
+ process.env.CDP_LISTEN_PORT = this.#remoteDebuggerPort;
659
+ } else {
660
+ this.#remoteDebuggerPort = process.env.CDP_LISTEN_PORT;
661
+ }
299
662
 
300
663
  // this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
301
- this.world = { attach: () => {} };
664
+ this.world = { attach: () => { } };
302
665
 
303
666
  const ai_config_file = path.join(this.projectDir, "ai_config.json");
304
667
  let ai_config = {};
@@ -318,7 +681,8 @@ export class BVTRecorder {
318
681
  ],
319
682
  };
320
683
 
321
- const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName);
684
+ const scenario = { pickle: this.scenarioDoc };
685
+ const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName, scenario);
322
686
  this.bvtContext = bvtContext;
323
687
  this.stepRunner = new BVTStepRunner({
324
688
  projectDir: this.projectDir,
@@ -344,8 +708,7 @@ export class BVTRecorder {
344
708
  },
345
709
  bvtContext: this.bvtContext,
346
710
  });
347
- const context = bvtContext.playContext;
348
- this.context = context;
711
+ this.context = bvtContext.playContext;
349
712
  this.web = bvtContext.stable || bvtContext.web;
350
713
  this.web.tryAllStrategies = true;
351
714
  this.page = bvtContext.page;
@@ -359,9 +722,19 @@ export class BVTRecorder {
359
722
  await this.context.exposeBinding(name, handler);
360
723
  }
361
724
  this._watchTestData();
362
- this.web.onRestoreSaveState = (url) => {
363
- this._initBrowser({ url });
725
+ this.web.onRestoreSaveState = async (url) => {
726
+ await this._initBrowser({ url });
727
+ this._addPagelisteners(this.context);
728
+ this._addFrameNavigateListener(this.page);
364
729
  };
730
+
731
+ // create a second browser for locator generation
732
+ this.backgroundBrowser = await chromium.launch({
733
+ headless: true,
734
+ });
735
+ this.backgroundContext = await this.backgroundBrowser.newContext({});
736
+ await this.backgroundContext.addInitScript({ content: this.getInitScripts(this.config) });
737
+ await this.backgroundContext.newPage();
365
738
  }
366
739
  async onClosePopup() {
367
740
  // console.log("close popups");
@@ -403,13 +776,14 @@ export class BVTRecorder {
403
776
 
404
777
  await this.page.goto(url, {
405
778
  waitUntil: "domcontentloaded",
779
+ timeout: this.config.page_timeout ?? 60_000,
406
780
  });
407
781
  // add listener for frame navigation on current tab
408
782
  this._addFrameNavigateListener(this.page);
409
783
 
410
784
  // eval init script on current tab
411
785
  // await this._initPage(this.page);
412
- this.#currentURL = new URL(url).pathname;
786
+ this.#currentURL = url;
413
787
 
414
788
  await this.page.dispatchEvent("html", "scroll");
415
789
  await delay(1000);
@@ -451,14 +825,15 @@ export class BVTRecorder {
451
825
  element: { inputID: "frame" },
452
826
  });
453
827
 
454
- const newPath = new URL(frame.url()).pathname;
828
+ const newUrl = frame.url();
829
+ const newPath = new URL(newUrl).pathname;
455
830
  const newTitle = await frame.title();
456
- if (newPath !== this.#currentURL) {
831
+ const changed = diffPaths(this.#currentURL, newUrl);
832
+
833
+ if (changed) {
457
834
  this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
458
- this.#currentURL = newPath;
835
+ this.#currentURL = newUrl;
459
836
  }
460
- // await this._setRecordingMode(frame);
461
- // await this._initPage(page);
462
837
  } catch (error) {
463
838
  this.logger.error("Error in frame navigate event");
464
839
  this.logger.error(error);
@@ -623,7 +998,6 @@ export class BVTRecorder {
623
998
  try {
624
999
  if (page.isClosed()) return;
625
1000
  this.pageSet.add(page);
626
-
627
1001
  await page.waitForLoadState("domcontentloaded");
628
1002
 
629
1003
  // add listener for frame navigation on new tab
@@ -688,6 +1062,52 @@ export class BVTRecorder {
688
1062
  console.error("Error in saving screenshot: ", error);
689
1063
  }
690
1064
  }
1065
+ async generateLocators(event) {
1066
+ const snapshotDetails = event.snapshotDetails;
1067
+ if (!snapshotDetails) {
1068
+ throw new Error("No snapshot details found");
1069
+ }
1070
+ const mode = event.mode;
1071
+ const inputID = event.element.inputID;
1072
+
1073
+ const { id, contextId, doc } = snapshotDetails;
1074
+ // const selector = `[data-blinq-id="${id}"]`;
1075
+ const newPage = await this.backgroundContext.newPage();
1076
+ await newPage.setContent(doc, { waitUntil: "domcontentloaded" });
1077
+ const locatorsObj = await newPage.evaluate(
1078
+ ([id, contextId, mode]) => {
1079
+ const recorder = window.__bvt_Recorder;
1080
+ const contextElement = document.querySelector(`[data-blinq-context-id="${contextId}"]`);
1081
+ const el = document.querySelector(`[data-blinq-id="${id}"]`);
1082
+ if (contextElement) {
1083
+ const result = recorder.locatorGenerator.toContextLocators(el, contextElement);
1084
+ return result;
1085
+ }
1086
+ const isRecordingText = mode === "recordingText";
1087
+ return recorder.locatorGenerator.getElementLocators(el, {
1088
+ excludeText: isRecordingText,
1089
+ });
1090
+ },
1091
+ [id, contextId, mode]
1092
+ );
1093
+
1094
+ // console.log(`Generated locators: for ${inputID}: `, JSON.stringify(locatorsObj));
1095
+ await newPage.close();
1096
+ if (event.nestFrmLoc?.children) {
1097
+ locatorsObj.nestFrmLoc = event.nestFrmLoc.children;
1098
+ }
1099
+
1100
+ this.sendEvent(this.events.updateCommand, {
1101
+ locators: {
1102
+ locators: locatorsObj.locators,
1103
+ nestFrmLoc: locatorsObj.nestFrmLoc,
1104
+ iframe_src: !event.frame.isTop ? event.frame.url : undefined,
1105
+ },
1106
+ allStrategyLocators: locatorsObj.allStrategyLocators,
1107
+ inputID,
1108
+ });
1109
+ // const
1110
+ }
691
1111
  async onAction(event) {
692
1112
  this._updateUrlPath();
693
1113
  // const locators = this.overlayLocators(event);
@@ -701,25 +1121,41 @@ export class BVTRecorder {
701
1121
  event.mode === "recordingHover",
702
1122
  event.mode === "multiInspecting"
703
1123
  ),
704
- locators: {
705
- locators: event.locators,
706
- iframe_src: !event.frame.isTop ? event.frame.url : undefined,
707
- },
708
- allStrategyLocators: event.allStrategyLocators,
1124
+ // locators: {
1125
+ // locators: event.locators,
1126
+ // iframe_src: !event.frame.isTop ? event.frame.url : undefined,
1127
+ // },
1128
+ // allStrategyLocators: event.allStrategyLocators,
709
1129
  url: event.frame.url,
710
1130
  title: event.frame.title,
711
1131
  extract: {},
712
1132
  lastKnownUrlPath: this.lastKnownUrlPath,
713
1133
  };
714
- if (event.nestFrmLoc?.children) {
715
- cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
716
- }
1134
+ // if (event.nestFrmLoc?.children) {
1135
+ // cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
1136
+ // }
717
1137
  // this.logger.info({ event });
718
1138
  if (this.shouldTakeScreenshot) {
719
1139
  await this.storeScreenshot(event);
720
1140
  }
721
- this.sendEvent(this.events.onNewCommand, cmdEvent);
722
- this._updateUrlPath();
1141
+ // this.sendEvent(this.events.onNewCommand, cmdEvent);
1142
+ // this._updateUrlPath();
1143
+ if (event.locators) {
1144
+ Object.assign(cmdEvent, {
1145
+ locators: {
1146
+ locators: event.locators,
1147
+ iframe_src: !event.frame.isTop ? event.frame.url : undefined,
1148
+ nestFrmLoc: event.nestFrmLoc?.children,
1149
+ },
1150
+ allStrategyLocators: event.allStrategyLocators,
1151
+ })
1152
+ this.sendEvent(this.events.onNewCommand, cmdEvent);
1153
+ this._updateUrlPath();
1154
+ } else {
1155
+ this.sendEvent(this.events.onNewCommand, cmdEvent);
1156
+ this._updateUrlPath();
1157
+ await this.generateLocators(event);
1158
+ }
723
1159
  }
724
1160
  _updateUrlPath() {
725
1161
  try {
@@ -735,13 +1171,12 @@ export class BVTRecorder {
735
1171
  }
736
1172
  async closeBrowser() {
737
1173
  delete process.env.TEMP_RUN;
738
- await this.watcher.close().then(() => {});
1174
+ await this.watcher.close().then(() => { });
739
1175
  this.watcher = null;
740
1176
  this.previousIndex = null;
741
1177
  this.previousHistoryLength = null;
742
1178
  this.previousUrl = null;
743
1179
  this.previousEntries = null;
744
-
745
1180
  await closeContext();
746
1181
  this.pageSet.clear();
747
1182
  }
@@ -764,25 +1199,26 @@ export class BVTRecorder {
764
1199
  for (let i = 0; i < 3; i++) {
765
1200
  result = 0;
766
1201
  try {
767
- for (const page of this.context.pages()) {
768
- for (const frame of page.frames()) {
769
- try {
770
- //scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params: Params)
771
- const frameResult = await this.web._locateElementByText(
772
- frame,
773
- searchString,
774
- tag,
775
- regex,
776
- partial,
777
- ignoreCase,
778
- {}
779
- );
780
- result += frameResult.elementCount;
781
- } catch (e) {
782
- console.log(e);
783
- }
1202
+ // for (const page of this.context.pages()) {
1203
+ const page = this.web.page;
1204
+ for (const frame of page.frames()) {
1205
+ try {
1206
+ //scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params: Params)
1207
+ const frameResult = await this.web._locateElementByText(
1208
+ frame,
1209
+ searchString,
1210
+ tag,
1211
+ regex,
1212
+ partial,
1213
+ ignoreCase,
1214
+ {}
1215
+ );
1216
+ result += frameResult.elementCount;
1217
+ } catch (e) {
1218
+ console.log(e);
784
1219
  }
785
1220
  }
1221
+ // }
786
1222
 
787
1223
  return result;
788
1224
  } catch (e) {
@@ -833,15 +1269,21 @@ export class BVTRecorder {
833
1269
  }, 100);
834
1270
  this.timerId = timerId;
835
1271
  }
836
- async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
1272
+ async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork, AICode }, options) {
837
1273
  const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
1274
+
1275
+ const env = path.basename(this.envName, ".json");
838
1276
  const _env = {
839
1277
  TOKEN: this.TOKEN,
840
1278
  TEMP_RUN: true,
841
1279
  REPORT_FOLDER: this.bvtContext.reportFolder,
842
1280
  BLINQ_ENV: this.envName,
843
1281
  DEBUG: "blinq:route",
1282
+ // BVT_TEMP_SNAPSHOTS_FOLDER: step.isImplemented ? path.join(this.tempSnapshotsFolder, env) : undefined,
844
1283
  };
1284
+ if (!step.isImplemented) {
1285
+ _env.BVT_TEMP_SNAPSHOTS_FOLDER = path.join(this.tempSnapshotsFolder, env);
1286
+ }
845
1287
 
846
1288
  this.bvtContext.navigate = true;
847
1289
  this.bvtContext.loadedRoutes = null;
@@ -861,6 +1303,7 @@ export class BVTRecorder {
861
1303
  await this.setMode("running");
862
1304
 
863
1305
  try {
1306
+ step.text = step.text.trim();
864
1307
  const { result, info } = await this.stepRunner.runStep(
865
1308
  {
866
1309
  step,
@@ -868,6 +1311,7 @@ export class BVTRecorder {
868
1311
  envPath: this.envName,
869
1312
  tags,
870
1313
  config: this.config,
1314
+ AICode,
871
1315
  },
872
1316
  this.bvtContext,
873
1317
  {
@@ -887,10 +1331,23 @@ export class BVTRecorder {
887
1331
  this.bvtContext.navigate = false;
888
1332
  }
889
1333
  }
890
- async saveScenario({ scenario, featureName, override, isSingleStep }) {
891
- await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
892
- if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
893
- await this.cleanup({ tags: scenario.tags });
1334
+ async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env, AICode }) {
1335
+ const res = await this.workspaceService.saveScenario({
1336
+ scenario,
1337
+ featureName,
1338
+ override,
1339
+ isSingleStep,
1340
+ branch,
1341
+ isEditing,
1342
+ projectId: path.basename(this.projectDir),
1343
+ env: env ?? this.envName,
1344
+ AICode,
1345
+ });
1346
+ if (res.success) {
1347
+ await this.cleanup({ tags: scenario.tags });
1348
+ } else {
1349
+ throw new Error(res.message || "Error saving scenario");
1350
+ }
894
1351
  }
895
1352
  async getImplementedSteps() {
896
1353
  const stepsAndScenarios = await getImplementedSteps(this.projectDir);
@@ -1055,9 +1512,11 @@ export class BVTRecorder {
1055
1512
  const featureFilePath = path.join(this.projectDir, "features", featureName);
1056
1513
  const gherkinDoc = this.parseFeatureFile(featureFilePath);
1057
1514
  const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
1515
+ this.scenarioDoc = scenario;
1058
1516
 
1059
1517
  const steps = [];
1060
1518
  const parameters = [];
1519
+ const datasets = [];
1061
1520
  if (scenario.examples && scenario.examples.length > 0) {
1062
1521
  const example = scenario.examples[0];
1063
1522
  example?.tableHeader?.cells.forEach((cell, index) => {
@@ -1065,7 +1524,26 @@ export class BVTRecorder {
1065
1524
  key: cell.value,
1066
1525
  value: unEscapeNonPrintables(example.tableBody[0].cells[index].value),
1067
1526
  });
1527
+ // datasets.push({
1528
+ // data: example.tableBody[]
1529
+ // })
1068
1530
  });
1531
+
1532
+ for (let i = 0; i < example.tableBody.length; i++) {
1533
+ const row = example.tableBody[i];
1534
+ // for (const row of example.tableBody) {
1535
+ const paramters = [];
1536
+ row.cells.forEach((cell, index) => {
1537
+ paramters.push({
1538
+ key: example.tableHeader.cells[index].value,
1539
+ value: unEscapeNonPrintables(cell.value),
1540
+ });
1541
+ });
1542
+ datasets.push({
1543
+ data: paramters,
1544
+ datasetId: i,
1545
+ });
1546
+ }
1069
1547
  }
1070
1548
 
1071
1549
  for (const step of scenario.steps) {
@@ -1086,6 +1564,7 @@ export class BVTRecorder {
1086
1564
  tags: scenario.tags.map((tag) => tag.name),
1087
1565
  steps,
1088
1566
  parameters,
1567
+ datasets,
1089
1568
  };
1090
1569
  }
1091
1570
  async findRelatedTextInAllFrames({ searchString, climb, contextText, params }) {
@@ -1118,6 +1597,271 @@ export class BVTRecorder {
1118
1597
  }
1119
1598
  return result;
1120
1599
  }
1600
+ async setStepCodeByScenario({
1601
+ function_name,
1602
+ mjs_file_content,
1603
+ user_request,
1604
+ selectedTarget,
1605
+ page_context,
1606
+ AIMemory,
1607
+ steps_context,
1608
+ }) {
1609
+ const runsURL = getRunsServiceBaseURL();
1610
+ const url = `${runsURL}/process-user-request/generate-code-with-context`;
1611
+ try {
1612
+ const result = await axiosClient({
1613
+ url,
1614
+ method: "POST",
1615
+ data: {
1616
+ function_name,
1617
+ mjs_file_content,
1618
+ user_request,
1619
+ selectedTarget,
1620
+ page_context,
1621
+ AIMemory,
1622
+ steps_context,
1623
+ },
1624
+ headers: {
1625
+ Authorization: `Bearer ${this.TOKEN}`,
1626
+ "X-Source": "recorder",
1627
+ },
1628
+ });
1629
+ if (result.status !== 200) {
1630
+ return { success: false, message: "Error while fetching code changes" };
1631
+ }
1632
+ return { success: true, data: result.data };
1633
+ } catch (error) {
1634
+ // @ts-ignore
1635
+ const reason = error?.response?.data?.error || "";
1636
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1637
+ throw new Error(`Failed to fetch code changes: ${errorMessage} \n ${reason}`);
1638
+ }
1639
+ }
1640
+
1641
+ async getStepCodeByScenario({ featureName, scenarioName, projectId, branch }) {
1642
+ try {
1643
+ const runsURL = getRunsServiceBaseURL();
1644
+ const ssoURL = runsURL.replace("/runs", "/auth");
1645
+ const privateRepoURL = `${ssoURL}/isRepoPrivate?project_id=${projectId}`;
1646
+
1647
+ const isPrivateRepoReq = await axiosClient({
1648
+ url: privateRepoURL,
1649
+ method: "GET",
1650
+ headers: {
1651
+ Authorization: `Bearer ${this.TOKEN}`,
1652
+ "X-Source": "recorder",
1653
+ },
1654
+ });
1655
+
1656
+ if (isPrivateRepoReq.status !== 200) {
1657
+ return { success: false, message: "Error while checking repo privacy" };
1658
+ }
1659
+
1660
+ const isPrivateRepo = isPrivateRepoReq.data.isPrivate ? isPrivateRepoReq.data.isPrivate : false;
1661
+
1662
+ const workspaceURL = runsURL.replace("/runs", "/workspace");
1663
+ const url = `${workspaceURL}/get-step-code-by-scenario`;
1664
+
1665
+ const result = await axiosClient({
1666
+ url,
1667
+ method: "POST",
1668
+ data: {
1669
+ scenarioName,
1670
+ featureName,
1671
+ projectId,
1672
+ isPrivateRepo,
1673
+ branch,
1674
+ },
1675
+ headers: {
1676
+ Authorization: `Bearer ${this.TOKEN}`,
1677
+ "X-Source": "recorder",
1678
+ },
1679
+ });
1680
+ if (result.status !== 200) {
1681
+ return { success: false, message: "Error while getting step code" };
1682
+ }
1683
+ return { success: true, data: result.data.stepInfo };
1684
+ } catch (error) {
1685
+ const reason = error?.response?.data?.error || "";
1686
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1687
+ throw new Error(`Failed to get step code: ${errorMessage} \n ${reason}`);
1688
+ }
1689
+ }
1690
+ async getContext() {
1691
+ await this.page.waitForLoadState("domcontentloaded");
1692
+ await this.page.waitForSelector("body");
1693
+ await this.page.waitForTimeout(500);
1694
+ return await this.page.evaluate(() => {
1695
+ return document.documentElement.outerHTML;
1696
+ });
1697
+ }
1698
+ async deleteCommandFromStepCode({ scenario, AICode, command }) {
1699
+ if (!AICode || AICode.length === 0) {
1700
+ console.log("No AI code available to delete.");
1701
+ return;
1702
+ }
1703
+
1704
+ const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
1705
+ const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
1706
+ process.env.tempFeaturesFolderPath = __temp_features_FolderName;
1707
+ process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
1708
+
1709
+ try {
1710
+ await this.stepRunner.copyCodetoTempFolder({ tempFolderPath, AICode });
1711
+ await this.stepRunner.writeWrapperCode(tempFolderPath);
1712
+ const codeView = AICode.find((f) => f.stepName === scenario.step.text);
1713
+
1714
+ if (!codeView) {
1715
+ throw new Error("Step code not found for step: " + scenario.step.text);
1716
+ }
1717
+
1718
+ const functionName = codeView.functionName;
1719
+ const mjsPath = path
1720
+ .normalize(codeView.mjsFile)
1721
+ .split(path.sep)
1722
+ .filter((part) => part !== "features")
1723
+ .join(path.sep);
1724
+ const codePath = path.join(tempFolderPath, mjsPath);
1725
+
1726
+ if (!existsSync(codePath)) {
1727
+ throw new Error("Step code file not found: " + codePath);
1728
+ }
1729
+
1730
+ const codePage = getCodePage(codePath);
1731
+
1732
+ const elements = codePage.getVariableDeclarationAsObject("elements");
1733
+
1734
+ const cucumberStep = getCucumberStep({ step: scenario.step });
1735
+ cucumberStep.text = scenario.step.text;
1736
+ const stepCommands = scenario.step.commands;
1737
+ const cmd = _toRecordingStep(command, scenario.step.name);
1738
+
1739
+ const recording = new Recording();
1740
+ recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
1741
+ const step = { ...recording.steps[0], ...cmd };
1742
+ const result = _generateCodeFromCommand(step, elements, {});
1743
+
1744
+ codePage._removeCommands(functionName, result.codeLines);
1745
+ codePage.removeUnusedElements();
1746
+ codePage.save();
1747
+
1748
+ await rm(tempFolderPath, { recursive: true, force: true });
1749
+
1750
+ return { code: codePage.fileContent, mjsFile: codeView.mjsFile };
1751
+ } catch (error) {
1752
+ await rm(tempFolderPath, { recursive: true, force: true });
1753
+ throw error;
1754
+ }
1755
+ }
1756
+ async addCommandToStepCode({ scenario, AICode }) {
1757
+ if (!AICode || AICode.length === 0) {
1758
+ console.log("No AI code available to add.");
1759
+ return;
1760
+ }
1761
+
1762
+ const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
1763
+ const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
1764
+ process.env.tempFeaturesFolderPath = __temp_features_FolderName;
1765
+ process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
1766
+
1767
+ try {
1768
+ await this.stepRunner.copyCodetoTempFolder({ tempFolderPath, AICode });
1769
+ await this.stepRunner.writeWrapperCode(tempFolderPath);
1770
+
1771
+ let codeView = AICode.find((f) => f.stepName === scenario.step.text);
1772
+
1773
+ if (codeView) {
1774
+ scenario.step.commands = [scenario.step.commands.pop()];
1775
+ const functionName = codeView.functionName;
1776
+ const mjsPath = path
1777
+ .normalize(codeView.mjsFile)
1778
+ .split(path.sep)
1779
+ .filter((part) => part !== "features")
1780
+ .join(path.sep);
1781
+ const codePath = path.join(tempFolderPath, mjsPath);
1782
+
1783
+ if (!existsSync(codePath)) {
1784
+ throw new Error("Step code file not found: " + codePath);
1785
+ }
1786
+
1787
+ const codePage = getCodePage(codePath);
1788
+ const elements = codePage.getVariableDeclarationAsObject("elements");
1789
+
1790
+ const cucumberStep = getCucumberStep({ step: scenario.step });
1791
+ cucumberStep.text = scenario.step.text;
1792
+ const stepCommands = scenario.step.commands;
1793
+ const cmd = _toRecordingStep(scenario.step.commands[0], scenario.step.name);
1794
+
1795
+ const recording = new Recording();
1796
+ recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
1797
+ const step = { ...recording.steps[0], ...cmd };
1798
+
1799
+ const result = _generateCodeFromCommand(step, elements, {});
1800
+ codePage.insertElements(result.elements);
1801
+
1802
+ codePage._injectOneCommand(functionName, result.codeLines.join("\n"));
1803
+ codePage.save();
1804
+
1805
+ await rm(tempFolderPath, { recursive: true, force: true });
1806
+
1807
+ return { code: codePage.fileContent, newStep: false, mjsFile: codeView.mjsFile };
1808
+ }
1809
+ console.log("Step code not found for step: ", scenario.step.text);
1810
+
1811
+ codeView = AICode[0];
1812
+ const functionName = toMethodName(scenario.step.text);
1813
+ const codeLines = [];
1814
+ const mjsPath = path
1815
+ .normalize(codeView.mjsFile)
1816
+ .split(path.sep)
1817
+ .filter((part) => part !== "features")
1818
+ .join(path.sep);
1819
+ const codePath = path.join(tempFolderPath, mjsPath);
1820
+
1821
+ if (!existsSync(codePath)) {
1822
+ throw new Error("Step code file not found: " + codePath);
1823
+ }
1824
+
1825
+ const codePage = getCodePage(codePath);
1826
+ const elements = codePage.getVariableDeclarationAsObject("elements");
1827
+ let newElements = { ...elements };
1828
+
1829
+ const cucumberStep = getCucumberStep({ step: scenario.step });
1830
+ cucumberStep.text = scenario.step.text;
1831
+ const stepCommands = scenario.step.commands;
1832
+ stepCommands.forEach((command) => {
1833
+ const cmd = _toRecordingStep(command, scenario.step.name);
1834
+
1835
+ const recording = new Recording();
1836
+ recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
1837
+ const step = { ...recording.steps[0], ...cmd };
1838
+ const result = _generateCodeFromCommand(step, elements, {});
1839
+ newElements = { ...result.elements };
1840
+ codeLines.push(...result.codeLines);
1841
+ });
1842
+
1843
+ codePage.insertElements(newElements);
1844
+ codePage.addInfraCommand(
1845
+ functionName,
1846
+ cucumberStep.text,
1847
+ cucumberStep.getVariablesList(),
1848
+ codeLines,
1849
+ false,
1850
+ "recorder"
1851
+ );
1852
+
1853
+ const keyword = (cucumberStep.keywordAlias ?? cucumberStep.keyword).trim();
1854
+ codePage.addCucumberStep(keyword, cucumberStep.getTemplate(), functionName, stepCommands.length);
1855
+ codePage.save();
1856
+
1857
+ await rm(tempFolderPath, { recursive: true, force: true });
1858
+
1859
+ return { code: codePage.fileContent, newStep: true, functionName, mjsFile: codeView.mjsFile };
1860
+ } catch (error) {
1861
+ await rm(tempFolderPath, { recursive: true, force: true });
1862
+ throw error;
1863
+ }
1864
+ }
1121
1865
  async cleanup({ tags }) {
1122
1866
  const noopStep = {
1123
1867
  text: "Noop",
@@ -1235,4 +1979,509 @@ export class BVTRecorder {
1235
1979
  this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
1236
1980
  }
1237
1981
  }
1982
+
1983
+ async fakeParams(params) {
1984
+ const newFakeParams = {};
1985
+ Object.keys(params).forEach((key) => {
1986
+ if (!params[key].startsWith("{{") || !params[key].endsWith("}}")) {
1987
+ newFakeParams[key] = params[key];
1988
+ return;
1989
+ }
1990
+
1991
+ try {
1992
+ const value = params[key].substring(2, params[key].length - 2).trim();
1993
+ const faking = value.split("(")[0].split(".");
1994
+ let argument = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
1995
+ argument = isNaN(Number(argument)) || argument === "" ? argument : Number(argument);
1996
+ let fakeFunc = faker;
1997
+ faking.forEach((f) => {
1998
+ fakeFunc = fakeFunc[f];
1999
+ });
2000
+ const newValue = fakeFunc(argument);
2001
+ newFakeParams[key] = newValue;
2002
+ } catch (error) {
2003
+ newFakeParams[key] = params[key];
2004
+ }
2005
+ });
2006
+
2007
+ return newFakeParams;
2008
+ }
2009
+
2010
+ async getBrowserState() {
2011
+ try {
2012
+ const state = await this.browserEmitter?.getState();
2013
+ this.sendEvent(this.events.browserStateSync, state);
2014
+ } catch (error) {
2015
+ this.logger.error("Error getting browser state:", error);
2016
+ this.sendEvent(this.events.browserStateError, {
2017
+ message: "Error getting browser state",
2018
+ code: "GET_STATE_ERROR",
2019
+ });
2020
+ }
2021
+ }
2022
+
2023
+ async applyClipboardPayload(message) {
2024
+ const payload = message?.data ?? message;
2025
+
2026
+ this.logger.info("[BVTRecorder] applyClipboardPayload called", {
2027
+ hasPayload: !!payload,
2028
+ hasText: !!payload?.text,
2029
+ hasHtml: !!payload?.html,
2030
+ trigger: message?.trigger,
2031
+ });
2032
+
2033
+ if (!payload) {
2034
+ this.logger.warn("[BVTRecorder] No payload provided");
2035
+ return;
2036
+ }
2037
+
2038
+ try {
2039
+ if (this.browserEmitter && typeof this.browserEmitter.applyClipboardPayload === "function") {
2040
+ this.logger.info("[BVTRecorder] Using RemoteBrowserService to apply clipboard");
2041
+ await this.browserEmitter.applyClipboardPayload(payload);
2042
+ return;
2043
+ }
2044
+
2045
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
2046
+ if (!activePage) {
2047
+ this.logger.warn("[BVTRecorder] No active page available");
2048
+ return;
2049
+ }
2050
+
2051
+ this.logger.info("[BVTRecorder] Applying clipboard to page", {
2052
+ url: activePage.url(),
2053
+ isClosed: activePage.isClosed(),
2054
+ });
2055
+
2056
+ const result = await activePage.evaluate((clipboardData) => {
2057
+ console.log("[Page] Executing clipboard application", clipboardData);
2058
+ if (typeof window.__bvt_applyClipboardData === "function") {
2059
+ return window.__bvt_applyClipboardData(clipboardData);
2060
+ }
2061
+ console.error("[Page] __bvt_applyClipboardData function not found!");
2062
+ return false;
2063
+ }, payload);
2064
+
2065
+ this.logger.info("[BVTRecorder] Clipboard application result:", result);
2066
+
2067
+ if (!result) {
2068
+ this.logger.warn("[BVTRecorder] Clipboard data not applied successfully");
2069
+ } else {
2070
+ this.logger.info("[BVTRecorder] Clipboard data applied successfully");
2071
+ }
2072
+ } catch (error) {
2073
+ this.logger.error("[BVTRecorder] Error applying clipboard payload", error);
2074
+ this.sendEvent(this.events.clipboardError, {
2075
+ message: "Failed to apply clipboard contents to the remote session",
2076
+ trigger: message?.trigger ?? "paste",
2077
+ });
2078
+ }
2079
+ }
2080
+
2081
+ hasClipboardPayload(payload) {
2082
+ return Boolean(
2083
+ payload && (payload.text || payload.html || (Array.isArray(payload.files) && payload.files.length > 0))
2084
+ );
2085
+ }
2086
+
2087
+ async collectClipboardFromPage(page) {
2088
+ if (!page) {
2089
+ this.logger.warn("[BVTRecorder] No page available to collect clipboard data");
2090
+ return null;
2091
+ }
2092
+ try {
2093
+ await page
2094
+ .context()
2095
+ .grantPermissions(["clipboard-read", "clipboard-write"])
2096
+ .catch((error) => {
2097
+ this.logger.warn("[BVTRecorder] Failed to grant clipboard permissions before read", error);
2098
+ });
2099
+
2100
+ const payload = await page.evaluate(async () => {
2101
+ const result = {};
2102
+ if (typeof navigator === "undefined" || !navigator.clipboard) {
2103
+ return result;
2104
+ }
2105
+
2106
+ const arrayBufferToBase64 = (buffer) => {
2107
+ let binary = "";
2108
+ const bytes = new Uint8Array(buffer);
2109
+ const chunkSize = 0x8000;
2110
+ for (let index = 0; index < bytes.length; index += chunkSize) {
2111
+ const chunk = bytes.subarray(index, index + chunkSize);
2112
+ binary += String.fromCharCode(...chunk);
2113
+ }
2114
+ return btoa(binary);
2115
+ };
2116
+
2117
+ const files = [];
2118
+
2119
+ if (typeof navigator.clipboard.read === "function") {
2120
+ try {
2121
+ const items = await navigator.clipboard.read();
2122
+ for (const item of items) {
2123
+ if (item.types.includes("text/html") && !result.html) {
2124
+ const blob = await item.getType("text/html");
2125
+ result.html = await blob.text();
2126
+ }
2127
+ if (item.types.includes("text/plain") && !result.text) {
2128
+ const blob = await item.getType("text/plain");
2129
+ result.text = await blob.text();
2130
+ }
2131
+ for (const type of item.types) {
2132
+ if (type.startsWith("text/")) {
2133
+ continue;
2134
+ }
2135
+ try {
2136
+ const blob = await item.getType(type);
2137
+ const buffer = await blob.arrayBuffer();
2138
+ files.push({
2139
+ name: `clipboard-file-${files.length + 1}`,
2140
+ type,
2141
+ lastModified: Date.now(),
2142
+ data: arrayBufferToBase64(buffer),
2143
+ });
2144
+ } catch (error) {
2145
+ console.warn("[BVTRecorder] Failed to serialize clipboard blob", { type, error });
2146
+ }
2147
+ }
2148
+ }
2149
+ } catch (error) {
2150
+ console.warn("[BVTRecorder] navigator.clipboard.read failed", error);
2151
+ }
2152
+ }
2153
+
2154
+ if (!result.text && typeof navigator.clipboard.readText === "function") {
2155
+ try {
2156
+ const text = await navigator.clipboard.readText();
2157
+ if (text) {
2158
+ result.text = text;
2159
+ }
2160
+ } catch (error) {
2161
+ console.warn("[BVTRecorder] navigator.clipboard.readText failed", error);
2162
+ }
2163
+ }
2164
+
2165
+ if (!result.text) {
2166
+ const selection = window.getSelection?.()?.toString?.();
2167
+ if (selection) {
2168
+ result.text = selection;
2169
+ }
2170
+ }
2171
+
2172
+ if (files.length > 0) {
2173
+ result.files = files;
2174
+ }
2175
+
2176
+ return result;
2177
+ });
2178
+
2179
+ return payload;
2180
+ } catch (error) {
2181
+ this.logger.error("[BVTRecorder] Error collecting clipboard payload", error);
2182
+ return null;
2183
+ }
2184
+ }
2185
+
2186
+ async readClipboardPayload(message) {
2187
+ try {
2188
+ let payload = null;
2189
+ if (this.browserEmitter && typeof this.browserEmitter.readClipboardPayload === "function") {
2190
+ payload = await this.browserEmitter.readClipboardPayload();
2191
+ } else {
2192
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
2193
+ payload = await this.collectClipboardFromPage(activePage);
2194
+ }
2195
+
2196
+ if (this.hasClipboardPayload(payload)) {
2197
+ this.logger.info("[BVTRecorder] Remote clipboard payload ready", {
2198
+ hasText: !!payload.text,
2199
+ hasHtml: !!payload.html,
2200
+ files: payload.files?.length ?? 0,
2201
+ });
2202
+ this.sendEvent(this.events.clipboardPush, {
2203
+ data: payload,
2204
+ trigger: message?.trigger ?? "copy",
2205
+ origin: message?.source ?? "browserUI",
2206
+ });
2207
+ return payload;
2208
+ }
2209
+
2210
+ this.logger.warn("[BVTRecorder] Remote clipboard payload empty or unavailable");
2211
+ this.sendEvent(this.events.clipboardError, {
2212
+ message: "Remote clipboard is empty",
2213
+ trigger: message?.trigger ?? "copy",
2214
+ });
2215
+ return null;
2216
+ } catch (error) {
2217
+ this.logger.error("[BVTRecorder] Error reading clipboard payload", error);
2218
+ this.sendEvent(this.events.clipboardError, {
2219
+ message: "Failed to read clipboard contents from the remote session",
2220
+ trigger: message?.trigger ?? "copy",
2221
+ details: error instanceof Error ? error.message : String(error),
2222
+ });
2223
+ throw error;
2224
+ }
2225
+ }
2226
+
2227
+ async injectClipboardIntoPage(page, payload) {
2228
+ if (!page) {
2229
+ return;
2230
+ }
2231
+
2232
+ try {
2233
+ await page
2234
+ .context()
2235
+ .grantPermissions(["clipboard-read", "clipboard-write"])
2236
+ .catch(() => { });
2237
+ await page.evaluate(async (clipboardPayload) => {
2238
+ const toArrayBuffer = (base64) => {
2239
+ if (!base64) {
2240
+ return null;
2241
+ }
2242
+ const binaryString = atob(base64);
2243
+ const len = binaryString.length;
2244
+ const bytes = new Uint8Array(len);
2245
+ for (let i = 0; i < len; i += 1) {
2246
+ bytes[i] = binaryString.charCodeAt(i);
2247
+ }
2248
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
2249
+ };
2250
+
2251
+ const createFileFromPayload = (filePayload) => {
2252
+ const buffer = toArrayBuffer(filePayload?.data);
2253
+ if (!buffer) {
2254
+ return null;
2255
+ }
2256
+ const name = filePayload?.name || "clipboard-file";
2257
+ const type = filePayload?.type || "application/octet-stream";
2258
+ const lastModified = filePayload?.lastModified ?? Date.now();
2259
+ try {
2260
+ return new File([buffer], name, { type, lastModified });
2261
+ } catch (error) {
2262
+ console.warn("Clipboard bridge could not recreate File object", error);
2263
+ return null;
2264
+ }
2265
+ };
2266
+
2267
+ let dataTransfer = null;
2268
+ try {
2269
+ dataTransfer = new DataTransfer();
2270
+ } catch (error) {
2271
+ console.warn("Clipboard bridge could not create DataTransfer", error);
2272
+ }
2273
+
2274
+ if (dataTransfer) {
2275
+ if (clipboardPayload?.text) {
2276
+ try {
2277
+ dataTransfer.setData("text/plain", clipboardPayload.text);
2278
+ } catch (error) {
2279
+ console.warn("Clipboard bridge failed to set text/plain", error);
2280
+ }
2281
+ }
2282
+ if (clipboardPayload?.html) {
2283
+ try {
2284
+ dataTransfer.setData("text/html", clipboardPayload.html);
2285
+ } catch (error) {
2286
+ console.warn("Clipboard bridge failed to set text/html", error);
2287
+ }
2288
+ }
2289
+ if (Array.isArray(clipboardPayload?.files)) {
2290
+ for (const filePayload of clipboardPayload.files) {
2291
+ const file = createFileFromPayload(filePayload);
2292
+ if (file) {
2293
+ try {
2294
+ dataTransfer.items.add(file);
2295
+ } catch (error) {
2296
+ console.warn("Clipboard bridge failed to append file", error);
2297
+ }
2298
+ }
2299
+ }
2300
+ }
2301
+ }
2302
+
2303
+ let target = document.activeElement || document.body;
2304
+ if (!target) {
2305
+ target = document.body || null;
2306
+ }
2307
+
2308
+ let pasteHandled = false;
2309
+ if (dataTransfer && target && typeof target.dispatchEvent === "function") {
2310
+ try {
2311
+ const clipboardEvent = new ClipboardEvent("paste", {
2312
+ clipboardData: dataTransfer,
2313
+ bubbles: true,
2314
+ cancelable: true,
2315
+ });
2316
+ pasteHandled = target.dispatchEvent(clipboardEvent);
2317
+ } catch (error) {
2318
+ console.warn("Clipboard bridge failed to dispatch synthetic paste event", error);
2319
+ }
2320
+ }
2321
+
2322
+ if (pasteHandled) {
2323
+ return;
2324
+ }
2325
+
2326
+ const callLegacyExecCommand = (command, value) => {
2327
+ const execCommand = document && document["execCommand"];
2328
+ if (typeof execCommand === "function") {
2329
+ try {
2330
+ return execCommand.call(document, command, false, value);
2331
+ } catch (error) {
2332
+ console.warn("Clipboard bridge failed to execute legacy command", error);
2333
+ }
2334
+ }
2335
+ return false;
2336
+ };
2337
+
2338
+ if (clipboardPayload?.html) {
2339
+ const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
2340
+ if (inserted) {
2341
+ return;
2342
+ }
2343
+ try {
2344
+ const selection = window.getSelection?.();
2345
+ if (selection && selection.rangeCount > 0) {
2346
+ const range = selection.getRangeAt(0);
2347
+ range.deleteContents();
2348
+ const fragment = range.createContextualFragment(clipboardPayload.html);
2349
+ range.insertNode(fragment);
2350
+ range.collapse(false);
2351
+ return;
2352
+ }
2353
+ } catch (error) {
2354
+ console.warn("Clipboard bridge could not insert HTML via Range APIs", error);
2355
+ }
2356
+ }
2357
+
2358
+ if (clipboardPayload?.text) {
2359
+ const inserted = callLegacyExecCommand("insertText", clipboardPayload.text);
2360
+ if (inserted) {
2361
+ return;
2362
+ }
2363
+ try {
2364
+ const selection = window.getSelection?.();
2365
+ if (selection && selection.rangeCount > 0) {
2366
+ const range = selection.getRangeAt(0);
2367
+ range.deleteContents();
2368
+ range.insertNode(document.createTextNode(clipboardPayload.text));
2369
+ range.collapse(false);
2370
+ return;
2371
+ }
2372
+ } catch (error) {
2373
+ console.warn("Clipboard bridge could not insert text via Range APIs", error);
2374
+ }
2375
+ }
2376
+
2377
+ if (clipboardPayload?.text && target && "value" in target) {
2378
+ try {
2379
+ const input = target;
2380
+ const start = input.selectionStart ?? input.value.length ?? 0;
2381
+ const end = input.selectionEnd ?? input.value.length ?? 0;
2382
+ const value = input.value ?? "";
2383
+ const text = clipboardPayload.text;
2384
+ input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
2385
+ const caret = start + text.length;
2386
+ if (typeof input.setSelectionRange === "function") {
2387
+ input.setSelectionRange(caret, caret);
2388
+ }
2389
+ input.dispatchEvent(new Event("input", { bubbles: true }));
2390
+ } catch (error) {
2391
+ console.warn("Clipboard bridge failed to mutate input element", error);
2392
+ }
2393
+ }
2394
+ }, payload);
2395
+ } catch (error) {
2396
+ throw error;
2397
+ }
2398
+ }
2399
+
2400
+ async createTab(url) {
2401
+ try {
2402
+ await this.browserEmitter?.createTab(url);
2403
+ } catch (error) {
2404
+ this.logger.error("Error creating tab:", error);
2405
+ this.sendEvent(this.events.browserStateError, {
2406
+ message: "Error creating tab",
2407
+ code: "CREATE_TAB_ERROR",
2408
+ });
2409
+ }
2410
+ }
2411
+
2412
+ async closeTab(pageId) {
2413
+ try {
2414
+ await this.browserEmitter?.closeTab(pageId);
2415
+ } catch (error) {
2416
+ this.logger.error("Error closing tab:", error);
2417
+ this.sendEvent(this.events.browserStateError, {
2418
+ message: "Error closing tab",
2419
+ code: "CLOSE_TAB_ERROR",
2420
+ });
2421
+ }
2422
+ }
2423
+
2424
+ async selectTab(pageId) {
2425
+ try {
2426
+ await this.browserEmitter?.selectTab(pageId);
2427
+ } catch (error) {
2428
+ this.logger.error("Error selecting tab:", error);
2429
+ this.sendEvent(this.events.browserStateError, {
2430
+ message: "Error selecting tab",
2431
+ code: "SELECT_TAB_ERROR",
2432
+ });
2433
+ }
2434
+ }
2435
+
2436
+ async navigateTab({ pageId, url }) {
2437
+ try {
2438
+ if (!pageId || !url) {
2439
+ this.logger.error("navigateTab called without pageId or url", { pageId, url });
2440
+ return;
2441
+ }
2442
+ await this.browserEmitter?.navigateTab(pageId, url);
2443
+ } catch (error) {
2444
+ this.logger.error("Error navigating tab:", error);
2445
+ this.sendEvent(this.events.browserStateError, {
2446
+ message: "Error navigating tab",
2447
+ code: "NAVIGATE_TAB_ERROR",
2448
+ });
2449
+ }
2450
+ }
2451
+
2452
+ async reloadTab(pageId) {
2453
+ try {
2454
+ await this.browserEmitter?.reloadTab(pageId);
2455
+ } catch (error) {
2456
+ this.logger.error("Error reloading tab:", error);
2457
+ this.sendEvent(this.events.browserStateError, {
2458
+ message: "Error reloading tab",
2459
+ code: "RELOAD_TAB_ERROR",
2460
+ });
2461
+ }
2462
+ }
2463
+
2464
+ async goBack(pageId) {
2465
+ try {
2466
+ await this.browserEmitter?.goBack(pageId);
2467
+ } catch (error) {
2468
+ this.logger.error("Error navigating back:", error);
2469
+ this.sendEvent(this.events.browserStateError, {
2470
+ message: "Error navigating back",
2471
+ code: "GO_BACK_ERROR",
2472
+ });
2473
+ }
2474
+ }
2475
+
2476
+ async goForward(pageId) {
2477
+ try {
2478
+ await this.browserEmitter?.goForward(pageId);
2479
+ } catch (error) {
2480
+ this.logger.error("Error navigating forward:", error);
2481
+ this.sendEvent(this.events.browserStateError, {
2482
+ message: "Error navigating forward",
2483
+ code: "GO_FORWARD_ERROR",
2484
+ });
2485
+ }
2486
+ }
1238
2487
  }