@dev-blinq/cucumber_client 1.0.1395-dev → 1.0.1395-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 (50) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +105 -105
  2. package/bin/assets/preload/css_gen.js +10 -10
  3. package/bin/assets/preload/toolbar.js +27 -29
  4. package/bin/assets/preload/unique_locators.js +1 -1
  5. package/bin/assets/preload/yaml.js +288 -275
  6. package/bin/assets/scripts/aria_snapshot.js +223 -220
  7. package/bin/assets/scripts/dom_attr.js +329 -329
  8. package/bin/assets/scripts/dom_parent.js +169 -174
  9. package/bin/assets/scripts/event_utils.js +94 -94
  10. package/bin/assets/scripts/pw.js +2050 -1949
  11. package/bin/assets/scripts/recorder.js +70 -45
  12. package/bin/assets/scripts/snapshot_capturer.js +147 -147
  13. package/bin/assets/scripts/unique_locators.js +163 -44
  14. package/bin/assets/scripts/yaml.js +796 -783
  15. package/bin/assets/templates/_hooks_template.txt +6 -2
  16. package/bin/assets/templates/utils_template.txt +1 -1
  17. package/bin/client/code_cleanup/find_step_definition_references.js +0 -2
  18. package/bin/client/code_cleanup/utils.js +5 -1
  19. package/bin/client/code_gen/api_codegen.js +2 -2
  20. package/bin/client/code_gen/code_inversion.js +63 -2
  21. package/bin/client/code_gen/function_signature.js +4 -0
  22. package/bin/client/code_gen/page_reflection.js +846 -906
  23. package/bin/client/code_gen/playwright_codeget.js +27 -3
  24. package/bin/client/cucumber/feature.js +4 -0
  25. package/bin/client/cucumber/feature_data.js +2 -2
  26. package/bin/client/cucumber/project_to_document.js +8 -2
  27. package/bin/client/cucumber/steps_definitions.js +6 -3
  28. package/bin/client/cucumber_selector.js +17 -1
  29. package/bin/client/local_agent.js +3 -2
  30. package/bin/client/parse_feature_file.js +23 -26
  31. package/bin/client/playground/projects/env.json +2 -2
  32. package/bin/client/project.js +186 -202
  33. package/bin/client/recorderv3/bvt_init.js +349 -0
  34. package/bin/client/recorderv3/bvt_recorder.js +1068 -104
  35. package/bin/client/recorderv3/implemented_steps.js +2 -0
  36. package/bin/client/recorderv3/index.js +4 -303
  37. package/bin/client/recorderv3/scriptTest.js +1 -1
  38. package/bin/client/recorderv3/services.js +814 -154
  39. package/bin/client/recorderv3/step_runner.js +315 -206
  40. package/bin/client/recorderv3/step_utils.js +473 -25
  41. package/bin/client/recorderv3/update_feature.js +9 -5
  42. package/bin/client/recorderv3/wbr_entry.js +61 -0
  43. package/bin/client/recording.js +1 -0
  44. package/bin/client/upload-service.js +3 -2
  45. package/bin/client/utils/socket_logger.js +132 -0
  46. package/bin/index.js +4 -1
  47. package/bin/logger.js +3 -2
  48. package/bin/min/consoleApi.min.cjs +2 -3
  49. package/bin/min/injectedScript.min.cjs +16 -16
  50. package/package.json +19 -9
@@ -4,22 +4,330 @@ import { existsSync, readdirSync, readFileSync, rmSync } from "fs";
4
4
  import path from "path";
5
5
  import url from "url";
6
6
  import { getImplementedSteps, parseRouteFiles } from "./implemented_steps.js";
7
- import { NamesService } from "./services.js";
7
+ import { NamesService, RemoteBrowserService, PublishService } from "./services.js";
8
8
  import { BVTStepRunner } from "./step_runner.js";
9
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 { loadStepDefinitions, getCommandsForImplementedStep } from "./step_utils.js";
12
11
  import { parseStepTextParameters } from "../cucumber/utils.js";
13
12
  import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
14
13
  import chokidar from "chokidar";
15
- import logger from "../../logger.js";
16
14
  import { unEscapeNonPrintables } from "../cucumber/utils.js";
17
15
  import { findAvailablePort } from "../utils/index.js";
18
- import { Step } from "../cucumber/feature.js";
16
+ import socketLogger from "../utils/socket_logger.js";
17
+ import { tmpdir } from "os";
18
+ import { faker } from "@faker-js/faker/locale/en_US";
19
+ import { chromium } from "playwright-core";
19
20
 
20
21
  const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
21
22
 
22
23
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
24
+
25
+ const clipboardBridgeScript = `
26
+ ;(() => {
27
+ if (window.__bvtRecorderClipboardBridgeInitialized) {
28
+ console.log('[ClipboardBridge] Already initialized, skipping');
29
+ return;
30
+ }
31
+ window.__bvtRecorderClipboardBridgeInitialized = true;
32
+ console.log('[ClipboardBridge] Initializing clipboard bridge');
33
+
34
+ const emitPayload = (payload, attempt = 0) => {
35
+ const reporter = window.__bvt_reportClipboard;
36
+ if (typeof reporter === "function") {
37
+ try {
38
+ console.log('[ClipboardBridge] Reporting clipboard payload:', payload);
39
+ reporter(payload);
40
+ } catch (error) {
41
+ console.warn("[ClipboardBridge] Failed to report payload", error);
42
+ }
43
+ return;
44
+ }
45
+ if (attempt < 5) {
46
+ console.log('[ClipboardBridge] Reporter not ready, retrying...', attempt);
47
+ setTimeout(() => emitPayload(payload, attempt + 1), 50 * (attempt + 1));
48
+ } else {
49
+ console.warn('[ClipboardBridge] Reporter never became available');
50
+ }
51
+ };
52
+
53
+ const fileToBase64 = (file) => {
54
+ return new Promise((resolve) => {
55
+ try {
56
+ const reader = new FileReader();
57
+ reader.onload = () => {
58
+ const { result } = reader;
59
+ if (typeof result === "string") {
60
+ const index = result.indexOf("base64,");
61
+ resolve(index !== -1 ? result.substring(index + 7) : result);
62
+ return;
63
+ }
64
+ if (result instanceof ArrayBuffer) {
65
+ const bytes = new Uint8Array(result);
66
+ let binary = "";
67
+ const chunk = 0x8000;
68
+ for (let i = 0; i < bytes.length; i += chunk) {
69
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
70
+ }
71
+ resolve(btoa(binary));
72
+ return;
73
+ }
74
+ resolve(null);
75
+ };
76
+ reader.onerror = () => resolve(null);
77
+ reader.readAsDataURL(file);
78
+ } catch (error) {
79
+ console.warn("[ClipboardBridge] Failed to serialize file", error);
80
+ resolve(null);
81
+ }
82
+ });
83
+ };
84
+
85
+ const handleClipboardEvent = async (event) => {
86
+ try {
87
+ console.log('[ClipboardBridge] Handling clipboard event:', event.type);
88
+ const payload = { trigger: event.type };
89
+ const clipboardData = event.clipboardData;
90
+
91
+ if (clipboardData) {
92
+ try {
93
+ const text = clipboardData.getData("text/plain");
94
+ if (text) {
95
+ payload.text = text;
96
+ console.log('[ClipboardBridge] Captured text:', text.substring(0, 50));
97
+ }
98
+ } catch (error) {
99
+ console.warn("[ClipboardBridge] Could not read text/plain", error);
100
+ }
101
+
102
+ try {
103
+ const html = clipboardData.getData("text/html");
104
+ if (html) {
105
+ payload.html = html;
106
+ console.log('[ClipboardBridge] Captured HTML:', html.substring(0, 50));
107
+ }
108
+ } catch (error) {
109
+ console.warn("[ClipboardBridge] Could not read text/html", error);
110
+ }
111
+
112
+ const files = clipboardData.files;
113
+ if (files && files.length > 0) {
114
+ console.log('[ClipboardBridge] Processing files:', files.length);
115
+ const serialized = [];
116
+ for (const file of files) {
117
+ const data = await fileToBase64(file);
118
+ if (data) {
119
+ serialized.push({
120
+ name: file.name,
121
+ type: file.type,
122
+ lastModified: file.lastModified,
123
+ data,
124
+ });
125
+ }
126
+ }
127
+ if (serialized.length > 0) {
128
+ payload.files = serialized;
129
+ }
130
+ }
131
+ }
132
+
133
+ if (!payload.text) {
134
+ try {
135
+ const selection = window.getSelection?.();
136
+ const selectionText = selection?.toString?.();
137
+ if (selectionText) {
138
+ payload.text = selectionText;
139
+ console.log('[ClipboardBridge] Using selection text:', selectionText.substring(0, 50));
140
+ }
141
+ } catch {
142
+ // Ignore selection access errors.
143
+ }
144
+ }
145
+
146
+ emitPayload(payload);
147
+ } catch (error) {
148
+ console.warn("[ClipboardBridge] Could not process event", error);
149
+ }
150
+ };
151
+
152
+ // NEW: Function to apply clipboard data to the page
153
+ window.__bvt_applyClipboardData = (payload) => {
154
+ console.log('[ClipboardBridge] Applying clipboard data:', payload);
155
+
156
+ if (!payload) {
157
+ console.warn('[ClipboardBridge] No payload provided');
158
+ return false;
159
+ }
160
+
161
+ try {
162
+ // Create DataTransfer object
163
+ let dataTransfer = null;
164
+ try {
165
+ dataTransfer = new DataTransfer();
166
+ console.log('[ClipboardBridge] DataTransfer created');
167
+ } catch (error) {
168
+ console.warn('[ClipboardBridge] Could not create DataTransfer', error);
169
+ }
170
+
171
+ if (dataTransfer) {
172
+ if (payload.text) {
173
+ try {
174
+ dataTransfer.setData("text/plain", payload.text);
175
+ console.log('[ClipboardBridge] Set text/plain:', payload.text.substring(0, 50));
176
+ } catch (error) {
177
+ console.warn('[ClipboardBridge] Failed to set text/plain', error);
178
+ }
179
+ }
180
+ if (payload.html) {
181
+ try {
182
+ dataTransfer.setData("text/html", payload.html);
183
+ console.log('[ClipboardBridge] Set text/html:', payload.html.substring(0, 50));
184
+ } catch (error) {
185
+ console.warn('[ClipboardBridge] Failed to set text/html', error);
186
+ }
187
+ }
188
+ }
189
+
190
+ // Get target element
191
+ let target = document.activeElement || document.body;
192
+ console.log('[ClipboardBridge] Target element:', {
193
+ tagName: target.tagName,
194
+ type: target.type,
195
+ isContentEditable: target.isContentEditable,
196
+ id: target.id,
197
+ className: target.className
198
+ });
199
+
200
+ // Try synthetic paste event first
201
+ let pasteHandled = false;
202
+ if (dataTransfer && target && typeof target.dispatchEvent === "function") {
203
+ try {
204
+ const pasteEvent = new ClipboardEvent("paste", {
205
+ clipboardData: dataTransfer,
206
+ bubbles: true,
207
+ cancelable: true,
208
+ });
209
+ pasteHandled = target.dispatchEvent(pasteEvent);
210
+ console.log('[ClipboardBridge] Paste event dispatched, handled:', pasteHandled);
211
+ } catch (error) {
212
+ console.warn('[ClipboardBridge] Failed to dispatch paste event', error);
213
+ }
214
+ }
215
+
216
+
217
+ console.log('[ClipboardBridge] Paste event not handled, trying fallback methods');
218
+
219
+ // Fallback: Try execCommand with HTML first (for contenteditable)
220
+ if (payload.html && target.isContentEditable) {
221
+ console.log('[ClipboardBridge] Trying execCommand insertHTML');
222
+ try {
223
+ const inserted = document.execCommand('insertHTML', false, payload.html);
224
+ if (inserted) {
225
+ console.log('[ClipboardBridge] Successfully inserted HTML via execCommand');
226
+ return true;
227
+ }
228
+ } catch (error) {
229
+ console.warn('[ClipboardBridge] execCommand insertHTML failed', error);
230
+ }
231
+
232
+ // Try Range API for HTML
233
+ console.log('[ClipboardBridge] Trying Range API for HTML');
234
+ try {
235
+ const selection = window.getSelection?.();
236
+ if (selection && selection.rangeCount > 0) {
237
+ const range = selection.getRangeAt(0);
238
+ range.deleteContents();
239
+ const fragment = range.createContextualFragment(payload.html);
240
+ range.insertNode(fragment);
241
+ range.collapse(false);
242
+ console.log('[ClipboardBridge] Successfully inserted HTML via Range API');
243
+ return true;
244
+ }
245
+ } catch (error) {
246
+ console.warn('[ClipboardBridge] Range API HTML insertion failed', error);
247
+ }
248
+ }
249
+
250
+ // Fallback: Try execCommand with text
251
+ if (payload.text) {
252
+ console.log('[ClipboardBridge] Trying execCommand insertText');
253
+ try {
254
+ const inserted = document.execCommand('insertText', false, payload.text);
255
+ if (inserted) {
256
+ console.log('[ClipboardBridge] Successfully inserted text via execCommand');
257
+ return true;
258
+ }
259
+ } catch (error) {
260
+ console.warn('[ClipboardBridge] execCommand insertText failed', error);
261
+ }
262
+
263
+ // Try Range API for text
264
+ if (target.isContentEditable) {
265
+ console.log('[ClipboardBridge] Trying Range API for text');
266
+ try {
267
+ const selection = window.getSelection?.();
268
+ if (selection && selection.rangeCount > 0) {
269
+ const range = selection.getRangeAt(0);
270
+ range.deleteContents();
271
+ range.insertNode(document.createTextNode(payload.text));
272
+ range.collapse(false);
273
+ console.log('[ClipboardBridge] Successfully inserted text via Range API');
274
+ return true;
275
+ }
276
+ } catch (error) {
277
+ console.warn('[ClipboardBridge] Range API text insertion failed', error);
278
+ }
279
+ }
280
+
281
+ // Last resort: Direct value assignment for input/textarea
282
+ if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
283
+ console.log('[ClipboardBridge] Trying direct value assignment');
284
+ try {
285
+ const start = target.selectionStart ?? target.value.length ?? 0;
286
+ const end = target.selectionEnd ?? target.value.length ?? 0;
287
+ const value = target.value ?? "";
288
+ const text = payload.text;
289
+ target.value = value.slice(0, start) + text + value.slice(end);
290
+ const caret = start + text.length;
291
+ if (typeof target.setSelectionRange === 'function') {
292
+ target.setSelectionRange(caret, caret);
293
+ }
294
+ target.dispatchEvent(new Event('input', { bubbles: true }));
295
+ console.log('[ClipboardBridge] Successfully set value directly');
296
+ return true;
297
+ } catch (error) {
298
+ console.warn('[ClipboardBridge] Direct value assignment failed', error);
299
+ }
300
+ }
301
+ }
302
+
303
+ console.warn('[ClipboardBridge] All paste methods failed');
304
+ return false;
305
+ } catch (error) {
306
+ console.error('[ClipboardBridge] Error applying clipboard data:', error);
307
+ return false;
308
+ }
309
+ };
310
+
311
+ // Set up event listeners for copy/cut
312
+ document.addEventListener(
313
+ "copy",
314
+ (event) => {
315
+ void handleClipboardEvent(event);
316
+ },
317
+ true
318
+ );
319
+ document.addEventListener(
320
+ "cut",
321
+ (event) => {
322
+ void handleClipboardEvent(event);
323
+ },
324
+ true
325
+ );
326
+
327
+ console.log('[ClipboardBridge] Clipboard bridge initialized successfully');
328
+ })();
329
+ `;
330
+
23
331
  export function getInitScript(config, options) {
24
332
  const preScript = `
25
333
  window.__bvt_Recorder_config = ${JSON.stringify(config ?? null)};
@@ -29,7 +337,7 @@ export function getInitScript(config, options) {
29
337
  path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"),
30
338
  "utf8"
31
339
  );
32
- return preScript + recorderScript;
340
+ return preScript + recorderScript + clipboardBridgeScript;
33
341
  }
34
342
 
35
343
  async function evaluate(frame, script) {
@@ -45,7 +353,6 @@ async function evaluate(frame, script) {
45
353
  async function findNestedFrameSelector(frame, obj) {
46
354
  try {
47
355
  const parent = frame.parentFrame();
48
- if (parent) console.log(`Parent frame: ${JSON.stringify(parent)}`);
49
356
  if (!parent) return { children: obj };
50
357
  const frameElement = await frame.frameElement();
51
358
  if (!frameElement) return;
@@ -54,6 +361,7 @@ async function findNestedFrameSelector(frame, obj) {
54
361
  }, frameElement);
55
362
  return findNestedFrameSelector(parent, { children: obj, selectors });
56
363
  } catch (e) {
364
+ socketLogger.error(`Error in findNestedFrameSelector: ${e}`);
57
365
  console.error(e);
58
366
  }
59
367
  }
@@ -150,17 +458,30 @@ const transformAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode,
150
458
  };
151
459
  }
152
460
  default: {
461
+ socketLogger.error(`Action not supported: ${action.name}`);
153
462
  console.log("action not supported", action);
154
463
  throw new Error("action not supported");
155
464
  }
156
465
  }
157
466
  };
467
+ const diffPaths = (currentPath, newPath) => {
468
+ const currentDomain = new URL(currentPath).hostname;
469
+ const newDomain = new URL(newPath).hostname;
470
+ if (currentDomain !== newDomain) {
471
+ return true;
472
+ } else {
473
+ const currentRoute = new URL(currentPath).pathname;
474
+ const newRoute = new URL(newPath).pathname;
475
+ return currentRoute !== newRoute;
476
+ }
477
+ };
158
478
  /**
159
479
  * @typedef {Object} BVTRecorderInput
160
480
  * @property {string} envName
161
481
  * @property {string} projectDir
162
482
  * @property {string} TOKEN
163
483
  * @property {(name:string, data:any)=> void} sendEvent
484
+ * @property {Object} logger
164
485
  */
165
486
  export class BVTRecorder {
166
487
  #currentURL = "";
@@ -174,7 +495,6 @@ export class BVTRecorder {
174
495
  */
175
496
  constructor(initialState) {
176
497
  Object.assign(this, initialState);
177
- this.logger = logger;
178
498
  this.screenshotMap = new Map();
179
499
  this.snapshotMap = new Map();
180
500
  this.scenariosStepsMap = new Map();
@@ -184,40 +504,19 @@ export class BVTRecorder {
184
504
  projectDir: this.projectDir,
185
505
  logger: this.logger,
186
506
  });
187
- this.stepRunner = new BVTStepRunner({
188
- projectDir: this.projectDir,
189
- sendExecutionStatus: (data) => {
190
- if (data && data.type) {
191
- switch (data.type) {
192
- case "cmdExecutionStart":
193
- console.log("Sending cmdExecutionStart event for cmdId:", data);
194
- this.sendEvent(this.events.cmdExecutionStart, data);
195
- break;
196
- case "cmdExecutionSuccess":
197
- console.log("Sending cmdExecutionSuccess event for cmdId:", data);
198
- this.sendEvent(this.events.cmdExecutionSuccess, data);
199
- break;
200
- case "cmdExecutionError":
201
- console.log("Sending cmdExecutionError event for cmdId:", data);
202
- this.sendEvent(this.events.cmdExecutionError, data);
203
- break;
204
- case "interceptResults":
205
- console.log("Sending interceptResults event");
206
- this.sendEvent(this.events.interceptResults, data);
207
- break;
208
- default:
209
- console.warn("Unknown command execution status type:", data.type);
210
- break;
211
- }
212
- }
213
- },
214
- });
507
+ this.workspaceService = new PublishService(this.TOKEN);
215
508
  this.pageSet = new Set();
216
509
  this.lastKnownUrlPath = "";
217
- // TODO: what is world?
218
510
  this.world = { attach: () => {} };
219
511
  this.shouldTakeScreenshot = true;
220
512
  this.watcher = null;
513
+ this.networkEventsFolder = path.join(tmpdir(), "blinq_network_events");
514
+ this.tempProjectFolder = `${tmpdir()}/bvt_temp_project_${Math.floor(Math.random() * 1000000)}`;
515
+ this.tempSnapshotsFolder = path.join(this.tempProjectFolder, "data/snapshots");
516
+
517
+ if (existsSync(this.networkEventsFolder)) {
518
+ rmSync(this.networkEventsFolder, { recursive: true, force: true });
519
+ }
221
520
  }
222
521
  events = {
223
522
  onFrameNavigate: "BVTRecorder.onFrameNavigate",
@@ -232,12 +531,18 @@ export class BVTRecorder {
232
531
  cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
233
532
  cmdExecutionError: "BVTRecorder.cmdExecutionError",
234
533
  interceptResults: "BVTRecorder.interceptResults",
534
+ onDebugURLChange: "BVTRecorder.onDebugURLChange",
535
+ updateCommand: "BVTRecorder.updateCommand",
536
+ browserStateSync: "BrowserService.stateSync",
537
+ browserStateError: "BrowserService.stateError",
538
+ clipboardPush: "BrowserService.clipboardPush",
539
+ clipboardError: "BrowserService.clipboardError",
235
540
  };
236
541
  bindings = {
237
542
  __bvt_recordCommand: async ({ frame, page, context }, event) => {
238
543
  this.#activeFrame = frame;
239
544
  const nestFrmLoc = await findNestedFrameSelector(frame);
240
- console.log(`Time taken for action: ${event.statistics.time}`);
545
+ this.logger.info(`Time taken for action: ${event.statistics.time}`);
241
546
  await this.onAction({ ...event, nestFrmLoc });
242
547
  },
243
548
  __bvt_getMode: async () => {
@@ -256,12 +561,30 @@ export class BVTRecorder {
256
561
  await this.onClosePopup();
257
562
  },
258
563
  __bvt_log: async (src, message) => {
259
- // this.logger.info(message);
260
- console.log(`Inside Browser: ${message}`);
564
+ this.logger.info(`Inside Browser: ${message}`);
261
565
  },
262
566
  __bvt_getObject: (_src, obj) => {
263
567
  this.processObject(obj);
264
568
  },
569
+ __bvt_reportClipboard: async ({ page }, payload) => {
570
+ try {
571
+ if (!payload) {
572
+ return;
573
+ }
574
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
575
+ if (activePage && activePage !== page) {
576
+ return;
577
+ }
578
+ const pageUrl = typeof page?.url === "function" ? page.url() : null;
579
+ this.sendEvent(this.events.clipboardPush, {
580
+ data: payload,
581
+ trigger: payload?.trigger ?? "copy",
582
+ pageUrl,
583
+ });
584
+ } catch (error) {
585
+ this.logger.error("Error forwarding clipboard payload from page", error);
586
+ }
587
+ },
265
588
  };
266
589
 
267
590
  getSnapshot = async (attr) => {
@@ -319,10 +642,14 @@ export class BVTRecorder {
319
642
  }
320
643
 
321
644
  async _initBrowser({ url }) {
322
- this.#remoteDebuggerPort = await findAvailablePort();
323
- process.env.CDP_LISTEN_PORT = this.#remoteDebuggerPort;
645
+ if (process.env.CDP_LISTEN_PORT === undefined) {
646
+ this.#remoteDebuggerPort = await findAvailablePort();
647
+ process.env.CDP_LISTEN_PORT = this.#remoteDebuggerPort;
648
+ } else {
649
+ this.#remoteDebuggerPort = process.env.CDP_LISTEN_PORT;
650
+ }
324
651
 
325
- this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
652
+ // this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
326
653
  this.world = { attach: () => {} };
327
654
 
328
655
  const ai_config_file = path.join(this.projectDir, "ai_config.json");
@@ -331,7 +658,7 @@ export class BVTRecorder {
331
658
  try {
332
659
  ai_config = JSON.parse(readFileSync(ai_config_file, "utf8"));
333
660
  } catch (error) {
334
- console.error("Error reading ai_config.json", error);
661
+ this.logger.error("Error reading ai_config.json", error);
335
662
  }
336
663
  }
337
664
  this.config = ai_config;
@@ -343,18 +670,48 @@ export class BVTRecorder {
343
670
  ],
344
671
  };
345
672
 
346
- let startTime = Date.now();
347
673
  const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName);
348
- let stopTime = Date.now();
349
- this.logger.info(`Browser launched in ${(stopTime - startTime) / 1000} s`);
350
674
  this.bvtContext = bvtContext;
351
- const context = bvtContext.playContext;
352
- this.context = context;
675
+ this.stepRunner = new BVTStepRunner({
676
+ projectDir: this.projectDir,
677
+ sendExecutionStatus: (data) => {
678
+ if (data && data.type) {
679
+ switch (data.type) {
680
+ case "cmdExecutionStart":
681
+ this.sendEvent(this.events.cmdExecutionStart, data);
682
+ break;
683
+ case "cmdExecutionSuccess":
684
+ this.sendEvent(this.events.cmdExecutionSuccess, data);
685
+ break;
686
+ case "cmdExecutionError":
687
+ this.sendEvent(this.events.cmdExecutionError, data);
688
+ break;
689
+ case "interceptResults":
690
+ this.sendEvent(this.events.interceptResults, data);
691
+ break;
692
+ default:
693
+ break;
694
+ }
695
+ }
696
+ },
697
+ bvtContext: this.bvtContext,
698
+ });
699
+ this.context = bvtContext.playContext;
353
700
  this.web = bvtContext.stable || bvtContext.web;
354
701
  this.web.tryAllStrategies = true;
355
702
  this.page = bvtContext.page;
356
-
357
703
  this.pageSet.add(this.page);
704
+ if (process.env.REMOTE_RECORDER === "true") {
705
+ this.browserEmitter = new RemoteBrowserService({
706
+ CDP_CONNECT_URL: `http://localhost:${this.#remoteDebuggerPort}`,
707
+ context: this.context,
708
+ });
709
+ this.browserEmitter.on(this.events.browserStateSync, (state) => {
710
+ this.page = this.browserEmitter.getSelectedPage();
711
+ this.sendEvent(this.events.browserStateSync, state);
712
+ });
713
+ }
714
+
358
715
  this.lastKnownUrlPath = this._updateUrlPath();
359
716
  const browser = await this.context.browser();
360
717
  this.browser = browser;
@@ -367,6 +724,14 @@ export class BVTRecorder {
367
724
  this.web.onRestoreSaveState = (url) => {
368
725
  this._initBrowser({ url });
369
726
  };
727
+
728
+ // create a second browser for locator generation
729
+ this.backgroundBrowser = await chromium.launch({
730
+ headless: true,
731
+ });
732
+ this.backgroundContext = await this.backgroundBrowser.newContext({});
733
+ await this.backgroundContext.addInitScript({ content: this.getInitScripts(this.config) });
734
+ await this.backgroundContext.newPage();
370
735
  }
371
736
  async onClosePopup() {
372
737
  // console.log("close popups");
@@ -381,13 +746,15 @@ export class BVTRecorder {
381
746
  }
382
747
  return;
383
748
  } catch (error) {
384
- console.error("Error evaluting in context:", error);
749
+ // console.error("Error evaluting in context:", error);
750
+ this.logger.error("Error evaluating in context:", error);
385
751
  }
386
752
  }
387
753
  }
388
754
 
389
755
  getMode() {
390
- console.log("getMode", this.#mode);
756
+ // console.log("getMode", this.#mode);
757
+ this.logger.info("Current mode:", this.#mode);
391
758
  return this.#mode;
392
759
  }
393
760
 
@@ -412,7 +779,7 @@ export class BVTRecorder {
412
779
 
413
780
  // eval init script on current tab
414
781
  // await this._initPage(this.page);
415
- this.#currentURL = new URL(url).pathname;
782
+ this.#currentURL = url;
416
783
 
417
784
  await this.page.dispatchEvent("html", "scroll");
418
785
  await delay(1000);
@@ -429,6 +796,8 @@ export class BVTRecorder {
429
796
  this.sendEvent(this.events.onBrowserClose);
430
797
  }
431
798
  } catch (error) {
799
+ this.logger.error("Error in page close event");
800
+ this.logger.error(error);
432
801
  console.error("Error in page close event");
433
802
  console.error(error);
434
803
  }
@@ -439,8 +808,10 @@ export class BVTRecorder {
439
808
  if (frame !== page.mainFrame()) return;
440
809
  this.handlePageTransition();
441
810
  } catch (error) {
811
+ this.logger.error("Error in handlePageTransition event");
812
+ this.logger.error(error);
442
813
  console.error("Error in handlePageTransition event");
443
- // console.error(error);
814
+ console.error(error);
444
815
  }
445
816
  try {
446
817
  if (frame !== this.#activeFrame) return;
@@ -450,15 +821,18 @@ export class BVTRecorder {
450
821
  element: { inputID: "frame" },
451
822
  });
452
823
 
453
- const newPath = new URL(frame.url()).pathname;
824
+ const newUrl = frame.url();
825
+ const newPath = new URL(newUrl).pathname;
454
826
  const newTitle = await frame.title();
455
- if (newPath !== this.#currentURL) {
827
+ const changed = diffPaths(this.#currentURL, newUrl);
828
+
829
+ if (changed) {
456
830
  this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
457
- this.#currentURL = newPath;
831
+ this.#currentURL = newUrl;
458
832
  }
459
- // await this._setRecordingMode(frame);
460
- // await this._initPage(page);
461
833
  } catch (error) {
834
+ this.logger.error("Error in frame navigate event");
835
+ this.logger.error(error);
462
836
  console.error("Error in frame navigate event");
463
837
  // console.error(error);
464
838
  }
@@ -541,13 +915,9 @@ export class BVTRecorder {
541
915
 
542
916
  try {
543
917
  const result = await client.send("Page.getNavigationHistory");
544
- // console.log("Navigation History:", result);
545
918
  const entries = result.entries;
546
919
  const currentIndex = result.currentIndex;
547
920
 
548
- // ignore if currentIndex is not the last entry
549
- // if (currentIndex !== entries.length - 1) return;
550
-
551
921
  const currentEntry = entries[currentIndex];
552
922
  const transitionInfo = this.analyzeTransitionType(entries, currentIndex, currentEntry);
553
923
  this.previousIndex = currentIndex;
@@ -560,6 +930,8 @@ export class BVTRecorder {
560
930
  navigationAction: transitionInfo.action,
561
931
  };
562
932
  } catch (error) {
933
+ this.logger.error("Error in getCurrentTransition event");
934
+ this.logger.error(error);
563
935
  console.error("Error in getTransistionType event", error);
564
936
  } finally {
565
937
  await client.detach();
@@ -622,12 +994,13 @@ export class BVTRecorder {
622
994
  try {
623
995
  if (page.isClosed()) return;
624
996
  this.pageSet.add(page);
625
-
626
997
  await page.waitForLoadState("domcontentloaded");
627
998
 
628
999
  // add listener for frame navigation on new tab
629
1000
  this._addFrameNavigateListener(page);
630
1001
  } catch (error) {
1002
+ this.logger.error("Error in page event");
1003
+ this.logger.error(error);
631
1004
  console.error("Error in page event");
632
1005
  console.error(error);
633
1006
  }
@@ -669,6 +1042,7 @@ export class BVTRecorder {
669
1042
  const { data } = await client.send("Page.captureScreenshot", { format: "png" });
670
1043
  return data;
671
1044
  } catch (error) {
1045
+ this.logger.error("Error in taking browser screenshot");
672
1046
  console.error("Error in taking browser screenshot", error);
673
1047
  } finally {
674
1048
  await client.detach();
@@ -684,6 +1058,52 @@ export class BVTRecorder {
684
1058
  console.error("Error in saving screenshot: ", error);
685
1059
  }
686
1060
  }
1061
+ async generateLocators(event) {
1062
+ const snapshotDetails = event.snapshotDetails;
1063
+ if (!snapshotDetails) {
1064
+ throw new Error("No snapshot details found");
1065
+ }
1066
+ const mode = event.mode;
1067
+ const inputID = event.element.inputID;
1068
+
1069
+ const { id, contextId, doc } = snapshotDetails;
1070
+ // const selector = `[data-blinq-id="${id}"]`;
1071
+ const newPage = await this.backgroundContext.newPage();
1072
+ await newPage.setContent(doc, { waitUntil: "domcontentloaded" });
1073
+ const locatorsObj = await newPage.evaluate(
1074
+ ([id, contextId, mode]) => {
1075
+ const recorder = window.__bvt_Recorder;
1076
+ const contextElement = document.querySelector(`[data-blinq-context-id="${contextId}"]`);
1077
+ const el = document.querySelector(`[data-blinq-id="${id}"]`);
1078
+ if (contextElement) {
1079
+ const result = recorder.locatorGenerator.toContextLocators(el, contextElement);
1080
+ return result;
1081
+ }
1082
+ const isRecordingText = mode === "recordingText";
1083
+ return recorder.locatorGenerator.getElementLocators(el, {
1084
+ excludeText: isRecordingText,
1085
+ });
1086
+ },
1087
+ [id, contextId, mode]
1088
+ );
1089
+
1090
+ // console.log(`Generated locators: for ${inputID}: `, JSON.stringify(locatorsObj));
1091
+ await newPage.close();
1092
+ if (event.nestFrmLoc?.children) {
1093
+ locatorsObj.nestFrmLoc = event.nestFrmLoc.children;
1094
+ }
1095
+
1096
+ this.sendEvent(this.events.updateCommand, {
1097
+ locators: {
1098
+ locators: locatorsObj.locators,
1099
+ nestFrmLoc: locatorsObj.nestFrmLoc,
1100
+ iframe_src: !event.frame.isTop ? event.frame.url : undefined,
1101
+ },
1102
+ allStrategyLocators: locatorsObj.allStrategyLocators,
1103
+ inputID,
1104
+ });
1105
+ // const
1106
+ }
687
1107
  async onAction(event) {
688
1108
  this._updateUrlPath();
689
1109
  // const locators = this.overlayLocators(event);
@@ -697,25 +1117,26 @@ export class BVTRecorder {
697
1117
  event.mode === "recordingHover",
698
1118
  event.mode === "multiInspecting"
699
1119
  ),
700
- locators: {
701
- locators: event.locators,
702
- iframe_src: !event.frame.isTop ? event.frame.url : undefined,
703
- },
704
- allStrategyLocators: event.allStrategyLocators,
1120
+ // locators: {
1121
+ // locators: event.locators,
1122
+ // iframe_src: !event.frame.isTop ? event.frame.url : undefined,
1123
+ // },
1124
+ // allStrategyLocators: event.allStrategyLocators,
705
1125
  url: event.frame.url,
706
1126
  title: event.frame.title,
707
1127
  extract: {},
708
1128
  lastKnownUrlPath: this.lastKnownUrlPath,
709
1129
  };
710
- if (event.nestFrmLoc?.children) {
711
- cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
712
- }
1130
+ // if (event.nestFrmLoc?.children) {
1131
+ // cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
1132
+ // }
713
1133
  // this.logger.info({ event });
714
1134
  if (this.shouldTakeScreenshot) {
715
1135
  await this.storeScreenshot(event);
716
1136
  }
717
1137
  this.sendEvent(this.events.onNewCommand, cmdEvent);
718
1138
  this._updateUrlPath();
1139
+ await this.generateLocators(event);
719
1140
  }
720
1141
  _updateUrlPath() {
721
1142
  try {
@@ -737,7 +1158,6 @@ export class BVTRecorder {
737
1158
  this.previousHistoryLength = null;
738
1159
  this.previousUrl = null;
739
1160
  this.previousEntries = null;
740
-
741
1161
  await closeContext();
742
1162
  this.pageSet.clear();
743
1163
  }
@@ -789,7 +1209,6 @@ export class BVTRecorder {
789
1209
  }
790
1210
 
791
1211
  async startRecordingInput() {
792
- console.log("startRecordingInput");
793
1212
  await this.setMode("recordingInput");
794
1213
  }
795
1214
  async stopRecordingInput() {
@@ -813,9 +1232,17 @@ export class BVTRecorder {
813
1232
  }
814
1233
 
815
1234
  async abortExecution() {
816
- this.bvtContext.web.abortedExecution = true;
817
1235
  await this.stepRunner.abortExecution();
818
1236
  }
1237
+
1238
+ async pauseExecution({ cmdId }) {
1239
+ await this.stepRunner.pauseExecution(cmdId);
1240
+ }
1241
+
1242
+ async resumeExecution({ cmdId }) {
1243
+ await this.stepRunner.resumeExecution(cmdId);
1244
+ }
1245
+
819
1246
  async dealyedRevertMode() {
820
1247
  const timerId = setTimeout(async () => {
821
1248
  await this.revertMode();
@@ -824,18 +1251,27 @@ export class BVTRecorder {
824
1251
  }
825
1252
  async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
826
1253
  const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
1254
+
1255
+ const env = path.basename(this.envName, ".json");
827
1256
  const _env = {
828
1257
  TOKEN: this.TOKEN,
829
1258
  TEMP_RUN: true,
830
1259
  REPORT_FOLDER: this.bvtContext.reportFolder,
831
1260
  BLINQ_ENV: this.envName,
832
- STORE_DETAILED_NETWORK_DATA: listenNetwork ? "true" : "false",
833
- CURRENT_STEP_ID: step.id,
1261
+ DEBUG: "blinq:route",
1262
+ // BVT_TEMP_SNAPSHOTS_FOLDER: step.isImplemented ? path.join(this.tempSnapshotsFolder, env) : undefined,
834
1263
  };
1264
+ if (!step.isImplemented) {
1265
+ _env.BVT_TEMP_SNAPSHOTS_FOLDER = path.join(this.tempSnapshotsFolder, env);
1266
+ }
835
1267
 
836
1268
  this.bvtContext.navigate = true;
837
1269
  this.bvtContext.loadedRoutes = null;
838
- this.bvtContext.web.abortedExecution = false;
1270
+ if (listenNetwork) {
1271
+ this.bvtContext.STORE_DETAILED_NETWORK_DATA = true;
1272
+ } else {
1273
+ this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
1274
+ }
839
1275
  for (const [key, value] of Object.entries(_env)) {
840
1276
  process.env[key] = value;
841
1277
  }
@@ -871,13 +1307,26 @@ export class BVTRecorder {
871
1307
  delete process.env[key];
872
1308
  }
873
1309
  this.bvtContext.navigate = false;
874
- this.bvtContext.web.abortedExecution = false;
875
1310
  }
876
1311
  }
877
- async saveScenario({ scenario, featureName, override, isSingleStep }) {
878
- await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
879
- if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
880
- await this.cleanup({ tags: scenario.tags });
1312
+ async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env }) {
1313
+ // await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
1314
+ // if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
1315
+ const res = await this.workspaceService.saveScenario({
1316
+ scenario,
1317
+ featureName,
1318
+ override,
1319
+ isSingleStep,
1320
+ branch,
1321
+ isEditing,
1322
+ projectId: path.basename(this.projectDir),
1323
+ env: env ?? this.envName,
1324
+ });
1325
+ if (res.success) {
1326
+ await this.cleanup({ tags: scenario.tags });
1327
+ } else {
1328
+ throw new Error(res.message || "Error saving scenario");
1329
+ }
881
1330
  }
882
1331
  async getImplementedSteps() {
883
1332
  const stepsAndScenarios = await getImplementedSteps(this.projectDir);
@@ -961,10 +1410,11 @@ export class BVTRecorder {
961
1410
  if (existsSync(_getDataFile(this.world, this.bvtContext, this.web))) {
962
1411
  try {
963
1412
  const testData = JSON.parse(readFileSync(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
964
- this.logger.info("Test data", testData);
1413
+ // this.logger.info("Test data", testData);
965
1414
  this.sendEvent(this.events.getTestData, testData);
966
1415
  } catch (e) {
967
- this.logger.error("Error reading test data file", e);
1416
+ // this.logger.error("Error reading test data file", e);
1417
+ console.log("Error reading test data file", e);
968
1418
  }
969
1419
  }
970
1420
 
@@ -973,10 +1423,12 @@ export class BVTRecorder {
973
1423
  this.watcher.on("all", async (event, path) => {
974
1424
  try {
975
1425
  const testData = JSON.parse(await readFile(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
976
- this.logger.info("Test data", testData);
1426
+ // this.logger.info("Test data", testData);
1427
+ console.log("Test data changed", testData);
977
1428
  this.sendEvent(this.events.getTestData, testData);
978
1429
  } catch (e) {
979
- this.logger.error("Error reading test data file", e);
1430
+ // this.logger.error("Error reading test data file", e);
1431
+ console.log("Error reading test data file", e);
980
1432
  }
981
1433
  });
982
1434
  }
@@ -1009,7 +1461,7 @@ export class BVTRecorder {
1009
1461
  .filter((file) => file.endsWith(".feature"))
1010
1462
  .map((file) => path.join(this.projectDir, "features", file));
1011
1463
  try {
1012
- const parsedFiles = featureFiles.map((file) => parseFeatureFile(file));
1464
+ const parsedFiles = featureFiles.map((file) => this.parseFeatureFile(file));
1013
1465
  const output = {};
1014
1466
  parsedFiles.forEach((file) => {
1015
1467
  if (!file.feature) return;
@@ -1037,7 +1489,7 @@ export class BVTRecorder {
1037
1489
  loadExistingScenario({ featureName, scenarioName }) {
1038
1490
  const step_definitions = loadStepDefinitions(this.projectDir);
1039
1491
  const featureFilePath = path.join(this.projectDir, "features", featureName);
1040
- const gherkinDoc = parseFeatureFile(featureFilePath);
1492
+ const gherkinDoc = this.parseFeatureFile(featureFilePath);
1041
1493
  const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
1042
1494
 
1043
1495
  const steps = [];
@@ -1196,20 +1648,532 @@ export class BVTRecorder {
1196
1648
  await this.cleanupExecution({ tags });
1197
1649
  await this.initExecution({ tags });
1198
1650
  }
1199
- }
1200
1651
 
1201
- const parseFeatureFile = (featureFilePath) => {
1202
- try {
1203
- let id = 0;
1204
- const uuidFn = () => (++id).toString(16);
1205
- const builder = new AstBuilder(uuidFn);
1206
- const matcher = new GherkinClassicTokenMatcher();
1207
- const parser = new Parser(builder, matcher);
1208
- const source = readFileSync(featureFilePath, "utf8");
1209
- const gherkinDocument = parser.parse(source);
1210
- return gherkinDocument;
1211
- } catch (e) {
1212
- console.log(e);
1652
+ parseFeatureFile(featureFilePath) {
1653
+ try {
1654
+ let id = 0;
1655
+ const uuidFn = () => (++id).toString(16);
1656
+ const builder = new AstBuilder(uuidFn);
1657
+ const matcher = new GherkinClassicTokenMatcher();
1658
+ const parser = new Parser(builder, matcher);
1659
+ const source = readFileSync(featureFilePath, "utf8");
1660
+ const gherkinDocument = parser.parse(source);
1661
+ return gherkinDocument;
1662
+ } catch (e) {
1663
+ this.logger.error(`Error parsing feature file: ${featureFilePath}`);
1664
+ console.log(e);
1665
+ }
1666
+ return {};
1213
1667
  }
1214
- return {};
1215
- };
1668
+
1669
+ stopRecordingNetwork(input) {
1670
+ if (this.bvtContext) {
1671
+ this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
1672
+ }
1673
+ }
1674
+
1675
+ async fakeParams(params) {
1676
+ const newFakeParams = {};
1677
+ Object.keys(params).forEach((key) => {
1678
+ if (!params[key].startsWith("{{") || !params[key].endsWith("}}")) {
1679
+ newFakeParams[key] = params[key];
1680
+ return;
1681
+ }
1682
+
1683
+ try {
1684
+ const value = params[key].substring(2, params[key].length - 2).trim();
1685
+ const faking = value.split("(")[0].split(".");
1686
+ let argument = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
1687
+ argument = isNaN(Number(argument)) || argument === "" ? argument : Number(argument);
1688
+ let fakeFunc = faker;
1689
+ faking.forEach((f) => {
1690
+ fakeFunc = fakeFunc[f];
1691
+ });
1692
+ const newValue = fakeFunc(argument);
1693
+ newFakeParams[key] = newValue;
1694
+ } catch (error) {
1695
+ newFakeParams[key] = params[key];
1696
+ }
1697
+ });
1698
+
1699
+ return newFakeParams;
1700
+ }
1701
+
1702
+ async getBrowserState() {
1703
+ try {
1704
+ const state = await this.browserEmitter?.getState();
1705
+ this.sendEvent(this.events.browserStateSync, state);
1706
+ } catch (error) {
1707
+ this.logger.error("Error getting browser state:", error);
1708
+ this.sendEvent(this.events.browserStateError, {
1709
+ message: "Error getting browser state",
1710
+ code: "GET_STATE_ERROR",
1711
+ });
1712
+ }
1713
+ }
1714
+
1715
+ async applyClipboardPayload(message) {
1716
+ const payload = message?.data ?? message;
1717
+
1718
+ this.logger.info("[BVTRecorder] applyClipboardPayload called", {
1719
+ hasPayload: !!payload,
1720
+ hasText: !!payload?.text,
1721
+ hasHtml: !!payload?.html,
1722
+ trigger: message?.trigger,
1723
+ });
1724
+
1725
+ if (!payload) {
1726
+ this.logger.warn("[BVTRecorder] No payload provided");
1727
+ return;
1728
+ }
1729
+
1730
+ try {
1731
+ if (this.browserEmitter && typeof this.browserEmitter.applyClipboardPayload === "function") {
1732
+ this.logger.info("[BVTRecorder] Using RemoteBrowserService to apply clipboard");
1733
+ await this.browserEmitter.applyClipboardPayload(payload);
1734
+ return;
1735
+ }
1736
+
1737
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
1738
+ if (!activePage) {
1739
+ this.logger.warn("[BVTRecorder] No active page available");
1740
+ return;
1741
+ }
1742
+
1743
+ this.logger.info("[BVTRecorder] Applying clipboard to page", {
1744
+ url: activePage.url(),
1745
+ isClosed: activePage.isClosed(),
1746
+ });
1747
+
1748
+ const result = await activePage.evaluate((clipboardData) => {
1749
+ console.log("[Page] Executing clipboard application", clipboardData);
1750
+ if (typeof window.__bvt_applyClipboardData === "function") {
1751
+ return window.__bvt_applyClipboardData(clipboardData);
1752
+ }
1753
+ console.error("[Page] __bvt_applyClipboardData function not found!");
1754
+ return false;
1755
+ }, payload);
1756
+
1757
+ this.logger.info("[BVTRecorder] Clipboard application result:", result);
1758
+
1759
+ if (!result) {
1760
+ this.logger.warn("[BVTRecorder] Clipboard data not applied successfully");
1761
+ } else {
1762
+ this.logger.info("[BVTRecorder] Clipboard data applied successfully");
1763
+ }
1764
+ } catch (error) {
1765
+ this.logger.error("[BVTRecorder] Error applying clipboard payload", error);
1766
+ this.sendEvent(this.events.clipboardError, {
1767
+ message: "Failed to apply clipboard contents to the remote session",
1768
+ trigger: message?.trigger ?? "paste",
1769
+ });
1770
+ }
1771
+ }
1772
+
1773
+ hasClipboardPayload(payload) {
1774
+ return Boolean(
1775
+ payload && (payload.text || payload.html || (Array.isArray(payload.files) && payload.files.length > 0))
1776
+ );
1777
+ }
1778
+
1779
+ async collectClipboardFromPage(page) {
1780
+ if (!page) {
1781
+ this.logger.warn("[BVTRecorder] No page available to collect clipboard data");
1782
+ return null;
1783
+ }
1784
+ try {
1785
+ await page
1786
+ .context()
1787
+ .grantPermissions(["clipboard-read", "clipboard-write"])
1788
+ .catch((error) => {
1789
+ this.logger.warn("[BVTRecorder] Failed to grant clipboard permissions before read", error);
1790
+ });
1791
+
1792
+ const payload = await page.evaluate(async () => {
1793
+ const result = {};
1794
+ if (typeof navigator === "undefined" || !navigator.clipboard) {
1795
+ return result;
1796
+ }
1797
+
1798
+ const arrayBufferToBase64 = (buffer) => {
1799
+ let binary = "";
1800
+ const bytes = new Uint8Array(buffer);
1801
+ const chunkSize = 0x8000;
1802
+ for (let index = 0; index < bytes.length; index += chunkSize) {
1803
+ const chunk = bytes.subarray(index, index + chunkSize);
1804
+ binary += String.fromCharCode(...chunk);
1805
+ }
1806
+ return btoa(binary);
1807
+ };
1808
+
1809
+ const files = [];
1810
+
1811
+ if (typeof navigator.clipboard.read === "function") {
1812
+ try {
1813
+ const items = await navigator.clipboard.read();
1814
+ for (const item of items) {
1815
+ if (item.types.includes("text/html") && !result.html) {
1816
+ const blob = await item.getType("text/html");
1817
+ result.html = await blob.text();
1818
+ }
1819
+ if (item.types.includes("text/plain") && !result.text) {
1820
+ const blob = await item.getType("text/plain");
1821
+ result.text = await blob.text();
1822
+ }
1823
+ for (const type of item.types) {
1824
+ if (type.startsWith("text/")) {
1825
+ continue;
1826
+ }
1827
+ try {
1828
+ const blob = await item.getType(type);
1829
+ const buffer = await blob.arrayBuffer();
1830
+ files.push({
1831
+ name: `clipboard-file-${files.length + 1}`,
1832
+ type,
1833
+ lastModified: Date.now(),
1834
+ data: arrayBufferToBase64(buffer),
1835
+ });
1836
+ } catch (error) {
1837
+ console.warn("[BVTRecorder] Failed to serialize clipboard blob", { type, error });
1838
+ }
1839
+ }
1840
+ }
1841
+ } catch (error) {
1842
+ console.warn("[BVTRecorder] navigator.clipboard.read failed", error);
1843
+ }
1844
+ }
1845
+
1846
+ if (!result.text && typeof navigator.clipboard.readText === "function") {
1847
+ try {
1848
+ const text = await navigator.clipboard.readText();
1849
+ if (text) {
1850
+ result.text = text;
1851
+ }
1852
+ } catch (error) {
1853
+ console.warn("[BVTRecorder] navigator.clipboard.readText failed", error);
1854
+ }
1855
+ }
1856
+
1857
+ if (!result.text) {
1858
+ const selection = window.getSelection?.()?.toString?.();
1859
+ if (selection) {
1860
+ result.text = selection;
1861
+ }
1862
+ }
1863
+
1864
+ if (files.length > 0) {
1865
+ result.files = files;
1866
+ }
1867
+
1868
+ return result;
1869
+ });
1870
+
1871
+ return payload;
1872
+ } catch (error) {
1873
+ this.logger.error("[BVTRecorder] Error collecting clipboard payload", error);
1874
+ return null;
1875
+ }
1876
+ }
1877
+
1878
+ async readClipboardPayload(message) {
1879
+ try {
1880
+ let payload = null;
1881
+ if (this.browserEmitter && typeof this.browserEmitter.readClipboardPayload === "function") {
1882
+ payload = await this.browserEmitter.readClipboardPayload();
1883
+ } else {
1884
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
1885
+ payload = await this.collectClipboardFromPage(activePage);
1886
+ }
1887
+
1888
+ if (this.hasClipboardPayload(payload)) {
1889
+ this.logger.info("[BVTRecorder] Remote clipboard payload ready", {
1890
+ hasText: !!payload.text,
1891
+ hasHtml: !!payload.html,
1892
+ files: payload.files?.length ?? 0,
1893
+ });
1894
+ this.sendEvent(this.events.clipboardPush, {
1895
+ data: payload,
1896
+ trigger: message?.trigger ?? "copy",
1897
+ origin: message?.source ?? "browserUI",
1898
+ });
1899
+ return payload;
1900
+ }
1901
+
1902
+ this.logger.warn("[BVTRecorder] Remote clipboard payload empty or unavailable");
1903
+ this.sendEvent(this.events.clipboardError, {
1904
+ message: "Remote clipboard is empty",
1905
+ trigger: message?.trigger ?? "copy",
1906
+ });
1907
+ return null;
1908
+ } catch (error) {
1909
+ this.logger.error("[BVTRecorder] Error reading clipboard payload", error);
1910
+ this.sendEvent(this.events.clipboardError, {
1911
+ message: "Failed to read clipboard contents from the remote session",
1912
+ trigger: message?.trigger ?? "copy",
1913
+ details: error instanceof Error ? error.message : String(error),
1914
+ });
1915
+ throw error;
1916
+ }
1917
+ }
1918
+
1919
+ async injectClipboardIntoPage(page, payload) {
1920
+ if (!page) {
1921
+ return;
1922
+ }
1923
+
1924
+ try {
1925
+ await page
1926
+ .context()
1927
+ .grantPermissions(["clipboard-read", "clipboard-write"])
1928
+ .catch(() => {});
1929
+ await page.evaluate(async (clipboardPayload) => {
1930
+ const toArrayBuffer = (base64) => {
1931
+ if (!base64) {
1932
+ return null;
1933
+ }
1934
+ const binaryString = atob(base64);
1935
+ const len = binaryString.length;
1936
+ const bytes = new Uint8Array(len);
1937
+ for (let i = 0; i < len; i += 1) {
1938
+ bytes[i] = binaryString.charCodeAt(i);
1939
+ }
1940
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
1941
+ };
1942
+
1943
+ const createFileFromPayload = (filePayload) => {
1944
+ const buffer = toArrayBuffer(filePayload?.data);
1945
+ if (!buffer) {
1946
+ return null;
1947
+ }
1948
+ const name = filePayload?.name || "clipboard-file";
1949
+ const type = filePayload?.type || "application/octet-stream";
1950
+ const lastModified = filePayload?.lastModified ?? Date.now();
1951
+ try {
1952
+ return new File([buffer], name, { type, lastModified });
1953
+ } catch (error) {
1954
+ console.warn("Clipboard bridge could not recreate File object", error);
1955
+ return null;
1956
+ }
1957
+ };
1958
+
1959
+ let dataTransfer = null;
1960
+ try {
1961
+ dataTransfer = new DataTransfer();
1962
+ } catch (error) {
1963
+ console.warn("Clipboard bridge could not create DataTransfer", error);
1964
+ }
1965
+
1966
+ if (dataTransfer) {
1967
+ if (clipboardPayload?.text) {
1968
+ try {
1969
+ dataTransfer.setData("text/plain", clipboardPayload.text);
1970
+ } catch (error) {
1971
+ console.warn("Clipboard bridge failed to set text/plain", error);
1972
+ }
1973
+ }
1974
+ if (clipboardPayload?.html) {
1975
+ try {
1976
+ dataTransfer.setData("text/html", clipboardPayload.html);
1977
+ } catch (error) {
1978
+ console.warn("Clipboard bridge failed to set text/html", error);
1979
+ }
1980
+ }
1981
+ if (Array.isArray(clipboardPayload?.files)) {
1982
+ for (const filePayload of clipboardPayload.files) {
1983
+ const file = createFileFromPayload(filePayload);
1984
+ if (file) {
1985
+ try {
1986
+ dataTransfer.items.add(file);
1987
+ } catch (error) {
1988
+ console.warn("Clipboard bridge failed to append file", error);
1989
+ }
1990
+ }
1991
+ }
1992
+ }
1993
+ }
1994
+
1995
+ let target = document.activeElement || document.body;
1996
+ if (!target) {
1997
+ target = document.body || null;
1998
+ }
1999
+
2000
+ let pasteHandled = false;
2001
+ if (dataTransfer && target && typeof target.dispatchEvent === "function") {
2002
+ try {
2003
+ const clipboardEvent = new ClipboardEvent("paste", {
2004
+ clipboardData: dataTransfer,
2005
+ bubbles: true,
2006
+ cancelable: true,
2007
+ });
2008
+ pasteHandled = target.dispatchEvent(clipboardEvent);
2009
+ } catch (error) {
2010
+ console.warn("Clipboard bridge failed to dispatch synthetic paste event", error);
2011
+ }
2012
+ }
2013
+
2014
+ if (pasteHandled) {
2015
+ return;
2016
+ }
2017
+
2018
+ const callLegacyExecCommand = (command, value) => {
2019
+ const execCommand = document && document["execCommand"];
2020
+ if (typeof execCommand === "function") {
2021
+ try {
2022
+ return execCommand.call(document, command, false, value);
2023
+ } catch (error) {
2024
+ console.warn("Clipboard bridge failed to execute legacy command", error);
2025
+ }
2026
+ }
2027
+ return false;
2028
+ };
2029
+
2030
+ if (clipboardPayload?.html) {
2031
+ const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
2032
+ if (inserted) {
2033
+ return;
2034
+ }
2035
+ try {
2036
+ const selection = window.getSelection?.();
2037
+ if (selection && selection.rangeCount > 0) {
2038
+ const range = selection.getRangeAt(0);
2039
+ range.deleteContents();
2040
+ const fragment = range.createContextualFragment(clipboardPayload.html);
2041
+ range.insertNode(fragment);
2042
+ range.collapse(false);
2043
+ return;
2044
+ }
2045
+ } catch (error) {
2046
+ console.warn("Clipboard bridge could not insert HTML via Range APIs", error);
2047
+ }
2048
+ }
2049
+
2050
+ if (clipboardPayload?.text) {
2051
+ const inserted = callLegacyExecCommand("insertText", clipboardPayload.text);
2052
+ if (inserted) {
2053
+ return;
2054
+ }
2055
+ try {
2056
+ const selection = window.getSelection?.();
2057
+ if (selection && selection.rangeCount > 0) {
2058
+ const range = selection.getRangeAt(0);
2059
+ range.deleteContents();
2060
+ range.insertNode(document.createTextNode(clipboardPayload.text));
2061
+ range.collapse(false);
2062
+ return;
2063
+ }
2064
+ } catch (error) {
2065
+ console.warn("Clipboard bridge could not insert text via Range APIs", error);
2066
+ }
2067
+ }
2068
+
2069
+ if (clipboardPayload?.text && target && "value" in target) {
2070
+ try {
2071
+ const input = target;
2072
+ const start = input.selectionStart ?? input.value.length ?? 0;
2073
+ const end = input.selectionEnd ?? input.value.length ?? 0;
2074
+ const value = input.value ?? "";
2075
+ const text = clipboardPayload.text;
2076
+ input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
2077
+ const caret = start + text.length;
2078
+ if (typeof input.setSelectionRange === "function") {
2079
+ input.setSelectionRange(caret, caret);
2080
+ }
2081
+ input.dispatchEvent(new Event("input", { bubbles: true }));
2082
+ } catch (error) {
2083
+ console.warn("Clipboard bridge failed to mutate input element", error);
2084
+ }
2085
+ }
2086
+ }, payload);
2087
+ } catch (error) {
2088
+ throw error;
2089
+ }
2090
+ }
2091
+
2092
+ async createTab(url) {
2093
+ try {
2094
+ await this.browserEmitter?.createTab(url);
2095
+ } catch (error) {
2096
+ this.logger.error("Error creating tab:", error);
2097
+ this.sendEvent(this.events.browserStateError, {
2098
+ message: "Error creating tab",
2099
+ code: "CREATE_TAB_ERROR",
2100
+ });
2101
+ }
2102
+ }
2103
+
2104
+ async closeTab(pageId) {
2105
+ try {
2106
+ await this.browserEmitter?.closeTab(pageId);
2107
+ } catch (error) {
2108
+ this.logger.error("Error closing tab:", error);
2109
+ this.sendEvent(this.events.browserStateError, {
2110
+ message: "Error closing tab",
2111
+ code: "CLOSE_TAB_ERROR",
2112
+ });
2113
+ }
2114
+ }
2115
+
2116
+ async selectTab(pageId) {
2117
+ try {
2118
+ await this.browserEmitter?.selectTab(pageId);
2119
+ } catch (error) {
2120
+ this.logger.error("Error selecting tab:", error);
2121
+ this.sendEvent(this.events.browserStateError, {
2122
+ message: "Error selecting tab",
2123
+ code: "SELECT_TAB_ERROR",
2124
+ });
2125
+ }
2126
+ }
2127
+
2128
+ async navigateTab({ pageId, url }) {
2129
+ try {
2130
+ if (!pageId || !url) {
2131
+ this.logger.error("navigateTab called without pageId or url", { pageId, url });
2132
+ return;
2133
+ }
2134
+ await this.browserEmitter?.navigateTab(pageId, url);
2135
+ } catch (error) {
2136
+ this.logger.error("Error navigating tab:", error);
2137
+ this.sendEvent(this.events.browserStateError, {
2138
+ message: "Error navigating tab",
2139
+ code: "NAVIGATE_TAB_ERROR",
2140
+ });
2141
+ }
2142
+ }
2143
+
2144
+ async reloadTab(pageId) {
2145
+ try {
2146
+ await this.browserEmitter?.reloadTab(pageId);
2147
+ } catch (error) {
2148
+ this.logger.error("Error reloading tab:", error);
2149
+ this.sendEvent(this.events.browserStateError, {
2150
+ message: "Error reloading tab",
2151
+ code: "RELOAD_TAB_ERROR",
2152
+ });
2153
+ }
2154
+ }
2155
+
2156
+ async goBack(pageId) {
2157
+ try {
2158
+ await this.browserEmitter?.goBack(pageId);
2159
+ } catch (error) {
2160
+ this.logger.error("Error navigating back:", error);
2161
+ this.sendEvent(this.events.browserStateError, {
2162
+ message: "Error navigating back",
2163
+ code: "GO_BACK_ERROR",
2164
+ });
2165
+ }
2166
+ }
2167
+
2168
+ async goForward(pageId) {
2169
+ try {
2170
+ await this.browserEmitter?.goForward(pageId);
2171
+ } catch (error) {
2172
+ this.logger.error("Error navigating forward:", error);
2173
+ this.sendEvent(this.events.browserStateError, {
2174
+ message: "Error navigating forward",
2175
+ code: "GO_FORWARD_ERROR",
2176
+ });
2177
+ }
2178
+ }
2179
+ }