@dev-blinq/cucumber_client 1.0.1412-dev → 1.0.1412-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 +16 -16
  17. package/bin/client/code_cleanup/find_step_definition_references.js +0 -1
  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 +19 -3
  28. package/bin/client/cucumber_selector.js +4 -0
  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 +363 -0
  34. package/bin/client/recorderv3/bvt_recorder.js +1056 -93
  35. package/bin/client/recorderv3/implemented_steps.js +2 -0
  36. package/bin/client/recorderv3/index.js +4 -311
  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 +36 -10
  40. package/bin/client/recorderv3/step_utils.js +495 -39
  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,12 +504,19 @@ export class BVTRecorder {
184
504
  projectDir: this.projectDir,
185
505
  logger: this.logger,
186
506
  });
507
+ this.workspaceService = new PublishService(this.TOKEN);
187
508
  this.pageSet = new Set();
188
509
  this.lastKnownUrlPath = "";
189
- // TODO: what is world?
190
510
  this.world = { attach: () => {} };
191
511
  this.shouldTakeScreenshot = true;
192
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
+ }
193
520
  }
194
521
  events = {
195
522
  onFrameNavigate: "BVTRecorder.onFrameNavigate",
@@ -204,12 +531,18 @@ export class BVTRecorder {
204
531
  cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
205
532
  cmdExecutionError: "BVTRecorder.cmdExecutionError",
206
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",
207
540
  };
208
541
  bindings = {
209
542
  __bvt_recordCommand: async ({ frame, page, context }, event) => {
210
543
  this.#activeFrame = frame;
211
544
  const nestFrmLoc = await findNestedFrameSelector(frame);
212
- console.log(`Time taken for action: ${event.statistics.time}`);
545
+ this.logger.info(`Time taken for action: ${event.statistics.time}`);
213
546
  await this.onAction({ ...event, nestFrmLoc });
214
547
  },
215
548
  __bvt_getMode: async () => {
@@ -228,12 +561,30 @@ export class BVTRecorder {
228
561
  await this.onClosePopup();
229
562
  },
230
563
  __bvt_log: async (src, message) => {
231
- // this.logger.info(message);
232
- console.log(`Inside Browser: ${message}`);
564
+ this.logger.info(`Inside Browser: ${message}`);
233
565
  },
234
566
  __bvt_getObject: (_src, obj) => {
235
567
  this.processObject(obj);
236
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
+ },
237
588
  };
238
589
 
239
590
  getSnapshot = async (attr) => {
@@ -291,8 +642,12 @@ export class BVTRecorder {
291
642
  }
292
643
 
293
644
  async _initBrowser({ url }) {
294
- this.#remoteDebuggerPort = await findAvailablePort();
295
- 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
+ }
296
651
 
297
652
  // this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
298
653
  this.world = { attach: () => {} };
@@ -303,7 +658,7 @@ export class BVTRecorder {
303
658
  try {
304
659
  ai_config = JSON.parse(readFileSync(ai_config_file, "utf8"));
305
660
  } catch (error) {
306
- console.error("Error reading ai_config.json", error);
661
+ this.logger.error("Error reading ai_config.json", error);
307
662
  }
308
663
  }
309
664
  this.config = ai_config;
@@ -315,10 +670,8 @@ export class BVTRecorder {
315
670
  ],
316
671
  };
317
672
 
318
- let startTime = Date.now();
319
- const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName);
320
- let stopTime = Date.now();
321
- this.logger.info(`Browser launched in ${(stopTime - startTime) / 1000} s`);
673
+ const scenario = { pickle: this.scenarioDoc };
674
+ const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName, scenario);
322
675
  this.bvtContext = bvtContext;
323
676
  this.stepRunner = new BVTStepRunner({
324
677
  projectDir: this.projectDir,
@@ -326,35 +679,40 @@ export class BVTRecorder {
326
679
  if (data && data.type) {
327
680
  switch (data.type) {
328
681
  case "cmdExecutionStart":
329
- console.log("Sending cmdExecutionStart event for cmdId:", data);
330
682
  this.sendEvent(this.events.cmdExecutionStart, data);
331
683
  break;
332
684
  case "cmdExecutionSuccess":
333
- console.log("Sending cmdExecutionSuccess event for cmdId:", data);
334
685
  this.sendEvent(this.events.cmdExecutionSuccess, data);
335
686
  break;
336
687
  case "cmdExecutionError":
337
- console.log("Sending cmdExecutionError event for cmdId:", data);
338
688
  this.sendEvent(this.events.cmdExecutionError, data);
339
689
  break;
340
690
  case "interceptResults":
341
- console.log("Sending interceptResults event");
342
691
  this.sendEvent(this.events.interceptResults, data);
343
692
  break;
344
693
  default:
345
- console.warn("Unknown command execution status type:", data.type);
346
694
  break;
347
695
  }
348
696
  }
349
697
  },
350
698
  bvtContext: this.bvtContext,
351
699
  });
352
- const context = bvtContext.playContext;
353
- this.context = context;
700
+ this.context = bvtContext.playContext;
354
701
  this.web = bvtContext.stable || bvtContext.web;
355
702
  this.web.tryAllStrategies = true;
356
703
  this.page = bvtContext.page;
357
704
  this.pageSet.add(this.page);
705
+ if (process.env.REMOTE_RECORDER === "true") {
706
+ this.browserEmitter = new RemoteBrowserService({
707
+ CDP_CONNECT_URL: `http://localhost:${this.#remoteDebuggerPort}`,
708
+ context: this.context,
709
+ });
710
+ this.browserEmitter.on(this.events.browserStateSync, (state) => {
711
+ this.page = this.browserEmitter.getSelectedPage();
712
+ this.sendEvent(this.events.browserStateSync, state);
713
+ });
714
+ }
715
+
358
716
  this.lastKnownUrlPath = this._updateUrlPath();
359
717
  const browser = await this.context.browser();
360
718
  this.browser = browser;
@@ -367,6 +725,14 @@ export class BVTRecorder {
367
725
  this.web.onRestoreSaveState = (url) => {
368
726
  this._initBrowser({ url });
369
727
  };
728
+
729
+ // create a second browser for locator generation
730
+ this.backgroundBrowser = await chromium.launch({
731
+ headless: true,
732
+ });
733
+ this.backgroundContext = await this.backgroundBrowser.newContext({});
734
+ await this.backgroundContext.addInitScript({ content: this.getInitScripts(this.config) });
735
+ await this.backgroundContext.newPage();
370
736
  }
371
737
  async onClosePopup() {
372
738
  // console.log("close popups");
@@ -381,13 +747,15 @@ export class BVTRecorder {
381
747
  }
382
748
  return;
383
749
  } catch (error) {
384
- console.error("Error evaluting in context:", error);
750
+ // console.error("Error evaluting in context:", error);
751
+ this.logger.error("Error evaluating in context:", error);
385
752
  }
386
753
  }
387
754
  }
388
755
 
389
756
  getMode() {
390
- console.log("getMode", this.#mode);
757
+ // console.log("getMode", this.#mode);
758
+ this.logger.info("Current mode:", this.#mode);
391
759
  return this.#mode;
392
760
  }
393
761
 
@@ -406,13 +774,14 @@ export class BVTRecorder {
406
774
 
407
775
  await this.page.goto(url, {
408
776
  waitUntil: "domcontentloaded",
777
+ timeout: this.config.page_timeout ?? 60_000,
409
778
  });
410
779
  // add listener for frame navigation on current tab
411
780
  this._addFrameNavigateListener(this.page);
412
781
 
413
782
  // eval init script on current tab
414
783
  // await this._initPage(this.page);
415
- this.#currentURL = new URL(url).pathname;
784
+ this.#currentURL = url;
416
785
 
417
786
  await this.page.dispatchEvent("html", "scroll");
418
787
  await delay(1000);
@@ -429,6 +798,8 @@ export class BVTRecorder {
429
798
  this.sendEvent(this.events.onBrowserClose);
430
799
  }
431
800
  } catch (error) {
801
+ this.logger.error("Error in page close event");
802
+ this.logger.error(error);
432
803
  console.error("Error in page close event");
433
804
  console.error(error);
434
805
  }
@@ -439,8 +810,10 @@ export class BVTRecorder {
439
810
  if (frame !== page.mainFrame()) return;
440
811
  this.handlePageTransition();
441
812
  } catch (error) {
813
+ this.logger.error("Error in handlePageTransition event");
814
+ this.logger.error(error);
442
815
  console.error("Error in handlePageTransition event");
443
- // console.error(error);
816
+ console.error(error);
444
817
  }
445
818
  try {
446
819
  if (frame !== this.#activeFrame) return;
@@ -450,15 +823,18 @@ export class BVTRecorder {
450
823
  element: { inputID: "frame" },
451
824
  });
452
825
 
453
- const newPath = new URL(frame.url()).pathname;
826
+ const newUrl = frame.url();
827
+ const newPath = new URL(newUrl).pathname;
454
828
  const newTitle = await frame.title();
455
- if (newPath !== this.#currentURL) {
829
+ const changed = diffPaths(this.#currentURL, newUrl);
830
+
831
+ if (changed) {
456
832
  this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
457
- this.#currentURL = newPath;
833
+ this.#currentURL = newUrl;
458
834
  }
459
- // await this._setRecordingMode(frame);
460
- // await this._initPage(page);
461
835
  } catch (error) {
836
+ this.logger.error("Error in frame navigate event");
837
+ this.logger.error(error);
462
838
  console.error("Error in frame navigate event");
463
839
  // console.error(error);
464
840
  }
@@ -541,13 +917,9 @@ export class BVTRecorder {
541
917
 
542
918
  try {
543
919
  const result = await client.send("Page.getNavigationHistory");
544
- // console.log("Navigation History:", result);
545
920
  const entries = result.entries;
546
921
  const currentIndex = result.currentIndex;
547
922
 
548
- // ignore if currentIndex is not the last entry
549
- // if (currentIndex !== entries.length - 1) return;
550
-
551
923
  const currentEntry = entries[currentIndex];
552
924
  const transitionInfo = this.analyzeTransitionType(entries, currentIndex, currentEntry);
553
925
  this.previousIndex = currentIndex;
@@ -560,6 +932,8 @@ export class BVTRecorder {
560
932
  navigationAction: transitionInfo.action,
561
933
  };
562
934
  } catch (error) {
935
+ this.logger.error("Error in getCurrentTransition event");
936
+ this.logger.error(error);
563
937
  console.error("Error in getTransistionType event", error);
564
938
  } finally {
565
939
  await client.detach();
@@ -622,12 +996,13 @@ export class BVTRecorder {
622
996
  try {
623
997
  if (page.isClosed()) return;
624
998
  this.pageSet.add(page);
625
-
626
999
  await page.waitForLoadState("domcontentloaded");
627
1000
 
628
1001
  // add listener for frame navigation on new tab
629
1002
  this._addFrameNavigateListener(page);
630
1003
  } catch (error) {
1004
+ this.logger.error("Error in page event");
1005
+ this.logger.error(error);
631
1006
  console.error("Error in page event");
632
1007
  console.error(error);
633
1008
  }
@@ -669,6 +1044,7 @@ export class BVTRecorder {
669
1044
  const { data } = await client.send("Page.captureScreenshot", { format: "png" });
670
1045
  return data;
671
1046
  } catch (error) {
1047
+ this.logger.error("Error in taking browser screenshot");
672
1048
  console.error("Error in taking browser screenshot", error);
673
1049
  } finally {
674
1050
  await client.detach();
@@ -684,6 +1060,52 @@ export class BVTRecorder {
684
1060
  console.error("Error in saving screenshot: ", error);
685
1061
  }
686
1062
  }
1063
+ async generateLocators(event) {
1064
+ const snapshotDetails = event.snapshotDetails;
1065
+ if (!snapshotDetails) {
1066
+ throw new Error("No snapshot details found");
1067
+ }
1068
+ const mode = event.mode;
1069
+ const inputID = event.element.inputID;
1070
+
1071
+ const { id, contextId, doc } = snapshotDetails;
1072
+ // const selector = `[data-blinq-id="${id}"]`;
1073
+ const newPage = await this.backgroundContext.newPage();
1074
+ await newPage.setContent(doc, { waitUntil: "domcontentloaded" });
1075
+ const locatorsObj = await newPage.evaluate(
1076
+ ([id, contextId, mode]) => {
1077
+ const recorder = window.__bvt_Recorder;
1078
+ const contextElement = document.querySelector(`[data-blinq-context-id="${contextId}"]`);
1079
+ const el = document.querySelector(`[data-blinq-id="${id}"]`);
1080
+ if (contextElement) {
1081
+ const result = recorder.locatorGenerator.toContextLocators(el, contextElement);
1082
+ return result;
1083
+ }
1084
+ const isRecordingText = mode === "recordingText";
1085
+ return recorder.locatorGenerator.getElementLocators(el, {
1086
+ excludeText: isRecordingText,
1087
+ });
1088
+ },
1089
+ [id, contextId, mode]
1090
+ );
1091
+
1092
+ // console.log(`Generated locators: for ${inputID}: `, JSON.stringify(locatorsObj));
1093
+ await newPage.close();
1094
+ if (event.nestFrmLoc?.children) {
1095
+ locatorsObj.nestFrmLoc = event.nestFrmLoc.children;
1096
+ }
1097
+
1098
+ this.sendEvent(this.events.updateCommand, {
1099
+ locators: {
1100
+ locators: locatorsObj.locators,
1101
+ nestFrmLoc: locatorsObj.nestFrmLoc,
1102
+ iframe_src: !event.frame.isTop ? event.frame.url : undefined,
1103
+ },
1104
+ allStrategyLocators: locatorsObj.allStrategyLocators,
1105
+ inputID,
1106
+ });
1107
+ // const
1108
+ }
687
1109
  async onAction(event) {
688
1110
  this._updateUrlPath();
689
1111
  // const locators = this.overlayLocators(event);
@@ -697,25 +1119,26 @@ export class BVTRecorder {
697
1119
  event.mode === "recordingHover",
698
1120
  event.mode === "multiInspecting"
699
1121
  ),
700
- locators: {
701
- locators: event.locators,
702
- iframe_src: !event.frame.isTop ? event.frame.url : undefined,
703
- },
704
- allStrategyLocators: event.allStrategyLocators,
1122
+ // locators: {
1123
+ // locators: event.locators,
1124
+ // iframe_src: !event.frame.isTop ? event.frame.url : undefined,
1125
+ // },
1126
+ // allStrategyLocators: event.allStrategyLocators,
705
1127
  url: event.frame.url,
706
1128
  title: event.frame.title,
707
1129
  extract: {},
708
1130
  lastKnownUrlPath: this.lastKnownUrlPath,
709
1131
  };
710
- if (event.nestFrmLoc?.children) {
711
- cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
712
- }
1132
+ // if (event.nestFrmLoc?.children) {
1133
+ // cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
1134
+ // }
713
1135
  // this.logger.info({ event });
714
1136
  if (this.shouldTakeScreenshot) {
715
1137
  await this.storeScreenshot(event);
716
1138
  }
717
1139
  this.sendEvent(this.events.onNewCommand, cmdEvent);
718
1140
  this._updateUrlPath();
1141
+ await this.generateLocators(event);
719
1142
  }
720
1143
  _updateUrlPath() {
721
1144
  try {
@@ -737,7 +1160,6 @@ export class BVTRecorder {
737
1160
  this.previousHistoryLength = null;
738
1161
  this.previousUrl = null;
739
1162
  this.previousEntries = null;
740
-
741
1163
  await closeContext();
742
1164
  this.pageSet.clear();
743
1165
  }
@@ -760,25 +1182,26 @@ export class BVTRecorder {
760
1182
  for (let i = 0; i < 3; i++) {
761
1183
  result = 0;
762
1184
  try {
763
- for (const page of this.context.pages()) {
764
- for (const frame of page.frames()) {
765
- try {
766
- //scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params: Params)
767
- const frameResult = await this.web._locateElementByText(
768
- frame,
769
- searchString,
770
- tag,
771
- regex,
772
- partial,
773
- ignoreCase,
774
- {}
775
- );
776
- result += frameResult.elementCount;
777
- } catch (e) {
778
- console.log(e);
779
- }
1185
+ // for (const page of this.context.pages()) {
1186
+ const page = this.web.page;
1187
+ for (const frame of page.frames()) {
1188
+ try {
1189
+ //scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params: Params)
1190
+ const frameResult = await this.web._locateElementByText(
1191
+ frame,
1192
+ searchString,
1193
+ tag,
1194
+ regex,
1195
+ partial,
1196
+ ignoreCase,
1197
+ {}
1198
+ );
1199
+ result += frameResult.elementCount;
1200
+ } catch (e) {
1201
+ console.log(e);
780
1202
  }
781
1203
  }
1204
+ // }
782
1205
 
783
1206
  return result;
784
1207
  } catch (e) {
@@ -831,17 +1254,27 @@ export class BVTRecorder {
831
1254
  }
832
1255
  async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
833
1256
  const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
1257
+
1258
+ const env = path.basename(this.envName, ".json");
834
1259
  const _env = {
835
1260
  TOKEN: this.TOKEN,
836
1261
  TEMP_RUN: true,
837
1262
  REPORT_FOLDER: this.bvtContext.reportFolder,
838
1263
  BLINQ_ENV: this.envName,
839
- STORE_DETAILED_NETWORK_DATA: listenNetwork ? "true" : "false",
840
- CURRENT_STEP_ID: step.id,
1264
+ DEBUG: "blinq:route",
1265
+ // BVT_TEMP_SNAPSHOTS_FOLDER: step.isImplemented ? path.join(this.tempSnapshotsFolder, env) : undefined,
841
1266
  };
1267
+ if (!step.isImplemented) {
1268
+ _env.BVT_TEMP_SNAPSHOTS_FOLDER = path.join(this.tempSnapshotsFolder, env);
1269
+ }
842
1270
 
843
1271
  this.bvtContext.navigate = true;
844
1272
  this.bvtContext.loadedRoutes = null;
1273
+ if (listenNetwork) {
1274
+ this.bvtContext.STORE_DETAILED_NETWORK_DATA = true;
1275
+ } else {
1276
+ this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
1277
+ }
845
1278
  for (const [key, value] of Object.entries(_env)) {
846
1279
  process.env[key] = value;
847
1280
  }
@@ -879,10 +1312,24 @@ export class BVTRecorder {
879
1312
  this.bvtContext.navigate = false;
880
1313
  }
881
1314
  }
882
- async saveScenario({ scenario, featureName, override, isSingleStep }) {
883
- await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
884
- if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
885
- await this.cleanup({ tags: scenario.tags });
1315
+ async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env }) {
1316
+ // await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
1317
+ // if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
1318
+ const res = await this.workspaceService.saveScenario({
1319
+ scenario,
1320
+ featureName,
1321
+ override,
1322
+ isSingleStep,
1323
+ branch,
1324
+ isEditing,
1325
+ projectId: path.basename(this.projectDir),
1326
+ env: env ?? this.envName,
1327
+ });
1328
+ if (res.success) {
1329
+ await this.cleanup({ tags: scenario.tags });
1330
+ } else {
1331
+ throw new Error(res.message || "Error saving scenario");
1332
+ }
886
1333
  }
887
1334
  async getImplementedSteps() {
888
1335
  const stepsAndScenarios = await getImplementedSteps(this.projectDir);
@@ -966,10 +1413,11 @@ export class BVTRecorder {
966
1413
  if (existsSync(_getDataFile(this.world, this.bvtContext, this.web))) {
967
1414
  try {
968
1415
  const testData = JSON.parse(readFileSync(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
969
- this.logger.info("Test data", testData);
1416
+ // this.logger.info("Test data", testData);
970
1417
  this.sendEvent(this.events.getTestData, testData);
971
1418
  } catch (e) {
972
- this.logger.error("Error reading test data file", e);
1419
+ // this.logger.error("Error reading test data file", e);
1420
+ console.log("Error reading test data file", e);
973
1421
  }
974
1422
  }
975
1423
 
@@ -978,10 +1426,12 @@ export class BVTRecorder {
978
1426
  this.watcher.on("all", async (event, path) => {
979
1427
  try {
980
1428
  const testData = JSON.parse(await readFile(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
981
- this.logger.info("Test data", testData);
1429
+ // this.logger.info("Test data", testData);
1430
+ console.log("Test data changed", testData);
982
1431
  this.sendEvent(this.events.getTestData, testData);
983
1432
  } catch (e) {
984
- this.logger.error("Error reading test data file", e);
1433
+ // this.logger.error("Error reading test data file", e);
1434
+ console.log("Error reading test data file", e);
985
1435
  }
986
1436
  });
987
1437
  }
@@ -1014,7 +1464,7 @@ export class BVTRecorder {
1014
1464
  .filter((file) => file.endsWith(".feature"))
1015
1465
  .map((file) => path.join(this.projectDir, "features", file));
1016
1466
  try {
1017
- const parsedFiles = featureFiles.map((file) => parseFeatureFile(file));
1467
+ const parsedFiles = featureFiles.map((file) => this.parseFeatureFile(file));
1018
1468
  const output = {};
1019
1469
  parsedFiles.forEach((file) => {
1020
1470
  if (!file.feature) return;
@@ -1042,8 +1492,9 @@ export class BVTRecorder {
1042
1492
  loadExistingScenario({ featureName, scenarioName }) {
1043
1493
  const step_definitions = loadStepDefinitions(this.projectDir);
1044
1494
  const featureFilePath = path.join(this.projectDir, "features", featureName);
1045
- const gherkinDoc = parseFeatureFile(featureFilePath);
1495
+ const gherkinDoc = this.parseFeatureFile(featureFilePath);
1046
1496
  const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
1497
+ this.scenarioDoc = scenario;
1047
1498
 
1048
1499
  const steps = [];
1049
1500
  const parameters = [];
@@ -1201,20 +1652,532 @@ export class BVTRecorder {
1201
1652
  await this.cleanupExecution({ tags });
1202
1653
  await this.initExecution({ tags });
1203
1654
  }
1204
- }
1205
1655
 
1206
- const parseFeatureFile = (featureFilePath) => {
1207
- try {
1208
- let id = 0;
1209
- const uuidFn = () => (++id).toString(16);
1210
- const builder = new AstBuilder(uuidFn);
1211
- const matcher = new GherkinClassicTokenMatcher();
1212
- const parser = new Parser(builder, matcher);
1213
- const source = readFileSync(featureFilePath, "utf8");
1214
- const gherkinDocument = parser.parse(source);
1215
- return gherkinDocument;
1216
- } catch (e) {
1217
- console.log(e);
1656
+ parseFeatureFile(featureFilePath) {
1657
+ try {
1658
+ let id = 0;
1659
+ const uuidFn = () => (++id).toString(16);
1660
+ const builder = new AstBuilder(uuidFn);
1661
+ const matcher = new GherkinClassicTokenMatcher();
1662
+ const parser = new Parser(builder, matcher);
1663
+ const source = readFileSync(featureFilePath, "utf8");
1664
+ const gherkinDocument = parser.parse(source);
1665
+ return gherkinDocument;
1666
+ } catch (e) {
1667
+ this.logger.error(`Error parsing feature file: ${featureFilePath}`);
1668
+ console.log(e);
1669
+ }
1670
+ return {};
1218
1671
  }
1219
- return {};
1220
- };
1672
+
1673
+ stopRecordingNetwork(input) {
1674
+ if (this.bvtContext) {
1675
+ this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
1676
+ }
1677
+ }
1678
+
1679
+ async fakeParams(params) {
1680
+ const newFakeParams = {};
1681
+ Object.keys(params).forEach((key) => {
1682
+ if (!params[key].startsWith("{{") || !params[key].endsWith("}}")) {
1683
+ newFakeParams[key] = params[key];
1684
+ return;
1685
+ }
1686
+
1687
+ try {
1688
+ const value = params[key].substring(2, params[key].length - 2).trim();
1689
+ const faking = value.split("(")[0].split(".");
1690
+ let argument = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
1691
+ argument = isNaN(Number(argument)) || argument === "" ? argument : Number(argument);
1692
+ let fakeFunc = faker;
1693
+ faking.forEach((f) => {
1694
+ fakeFunc = fakeFunc[f];
1695
+ });
1696
+ const newValue = fakeFunc(argument);
1697
+ newFakeParams[key] = newValue;
1698
+ } catch (error) {
1699
+ newFakeParams[key] = params[key];
1700
+ }
1701
+ });
1702
+
1703
+ return newFakeParams;
1704
+ }
1705
+
1706
+ async getBrowserState() {
1707
+ try {
1708
+ const state = await this.browserEmitter?.getState();
1709
+ this.sendEvent(this.events.browserStateSync, state);
1710
+ } catch (error) {
1711
+ this.logger.error("Error getting browser state:", error);
1712
+ this.sendEvent(this.events.browserStateError, {
1713
+ message: "Error getting browser state",
1714
+ code: "GET_STATE_ERROR",
1715
+ });
1716
+ }
1717
+ }
1718
+
1719
+ async applyClipboardPayload(message) {
1720
+ const payload = message?.data ?? message;
1721
+
1722
+ this.logger.info("[BVTRecorder] applyClipboardPayload called", {
1723
+ hasPayload: !!payload,
1724
+ hasText: !!payload?.text,
1725
+ hasHtml: !!payload?.html,
1726
+ trigger: message?.trigger,
1727
+ });
1728
+
1729
+ if (!payload) {
1730
+ this.logger.warn("[BVTRecorder] No payload provided");
1731
+ return;
1732
+ }
1733
+
1734
+ try {
1735
+ if (this.browserEmitter && typeof this.browserEmitter.applyClipboardPayload === "function") {
1736
+ this.logger.info("[BVTRecorder] Using RemoteBrowserService to apply clipboard");
1737
+ await this.browserEmitter.applyClipboardPayload(payload);
1738
+ return;
1739
+ }
1740
+
1741
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
1742
+ if (!activePage) {
1743
+ this.logger.warn("[BVTRecorder] No active page available");
1744
+ return;
1745
+ }
1746
+
1747
+ this.logger.info("[BVTRecorder] Applying clipboard to page", {
1748
+ url: activePage.url(),
1749
+ isClosed: activePage.isClosed(),
1750
+ });
1751
+
1752
+ const result = await activePage.evaluate((clipboardData) => {
1753
+ console.log("[Page] Executing clipboard application", clipboardData);
1754
+ if (typeof window.__bvt_applyClipboardData === "function") {
1755
+ return window.__bvt_applyClipboardData(clipboardData);
1756
+ }
1757
+ console.error("[Page] __bvt_applyClipboardData function not found!");
1758
+ return false;
1759
+ }, payload);
1760
+
1761
+ this.logger.info("[BVTRecorder] Clipboard application result:", result);
1762
+
1763
+ if (!result) {
1764
+ this.logger.warn("[BVTRecorder] Clipboard data not applied successfully");
1765
+ } else {
1766
+ this.logger.info("[BVTRecorder] Clipboard data applied successfully");
1767
+ }
1768
+ } catch (error) {
1769
+ this.logger.error("[BVTRecorder] Error applying clipboard payload", error);
1770
+ this.sendEvent(this.events.clipboardError, {
1771
+ message: "Failed to apply clipboard contents to the remote session",
1772
+ trigger: message?.trigger ?? "paste",
1773
+ });
1774
+ }
1775
+ }
1776
+
1777
+ hasClipboardPayload(payload) {
1778
+ return Boolean(
1779
+ payload && (payload.text || payload.html || (Array.isArray(payload.files) && payload.files.length > 0))
1780
+ );
1781
+ }
1782
+
1783
+ async collectClipboardFromPage(page) {
1784
+ if (!page) {
1785
+ this.logger.warn("[BVTRecorder] No page available to collect clipboard data");
1786
+ return null;
1787
+ }
1788
+ try {
1789
+ await page
1790
+ .context()
1791
+ .grantPermissions(["clipboard-read", "clipboard-write"])
1792
+ .catch((error) => {
1793
+ this.logger.warn("[BVTRecorder] Failed to grant clipboard permissions before read", error);
1794
+ });
1795
+
1796
+ const payload = await page.evaluate(async () => {
1797
+ const result = {};
1798
+ if (typeof navigator === "undefined" || !navigator.clipboard) {
1799
+ return result;
1800
+ }
1801
+
1802
+ const arrayBufferToBase64 = (buffer) => {
1803
+ let binary = "";
1804
+ const bytes = new Uint8Array(buffer);
1805
+ const chunkSize = 0x8000;
1806
+ for (let index = 0; index < bytes.length; index += chunkSize) {
1807
+ const chunk = bytes.subarray(index, index + chunkSize);
1808
+ binary += String.fromCharCode(...chunk);
1809
+ }
1810
+ return btoa(binary);
1811
+ };
1812
+
1813
+ const files = [];
1814
+
1815
+ if (typeof navigator.clipboard.read === "function") {
1816
+ try {
1817
+ const items = await navigator.clipboard.read();
1818
+ for (const item of items) {
1819
+ if (item.types.includes("text/html") && !result.html) {
1820
+ const blob = await item.getType("text/html");
1821
+ result.html = await blob.text();
1822
+ }
1823
+ if (item.types.includes("text/plain") && !result.text) {
1824
+ const blob = await item.getType("text/plain");
1825
+ result.text = await blob.text();
1826
+ }
1827
+ for (const type of item.types) {
1828
+ if (type.startsWith("text/")) {
1829
+ continue;
1830
+ }
1831
+ try {
1832
+ const blob = await item.getType(type);
1833
+ const buffer = await blob.arrayBuffer();
1834
+ files.push({
1835
+ name: `clipboard-file-${files.length + 1}`,
1836
+ type,
1837
+ lastModified: Date.now(),
1838
+ data: arrayBufferToBase64(buffer),
1839
+ });
1840
+ } catch (error) {
1841
+ console.warn("[BVTRecorder] Failed to serialize clipboard blob", { type, error });
1842
+ }
1843
+ }
1844
+ }
1845
+ } catch (error) {
1846
+ console.warn("[BVTRecorder] navigator.clipboard.read failed", error);
1847
+ }
1848
+ }
1849
+
1850
+ if (!result.text && typeof navigator.clipboard.readText === "function") {
1851
+ try {
1852
+ const text = await navigator.clipboard.readText();
1853
+ if (text) {
1854
+ result.text = text;
1855
+ }
1856
+ } catch (error) {
1857
+ console.warn("[BVTRecorder] navigator.clipboard.readText failed", error);
1858
+ }
1859
+ }
1860
+
1861
+ if (!result.text) {
1862
+ const selection = window.getSelection?.()?.toString?.();
1863
+ if (selection) {
1864
+ result.text = selection;
1865
+ }
1866
+ }
1867
+
1868
+ if (files.length > 0) {
1869
+ result.files = files;
1870
+ }
1871
+
1872
+ return result;
1873
+ });
1874
+
1875
+ return payload;
1876
+ } catch (error) {
1877
+ this.logger.error("[BVTRecorder] Error collecting clipboard payload", error);
1878
+ return null;
1879
+ }
1880
+ }
1881
+
1882
+ async readClipboardPayload(message) {
1883
+ try {
1884
+ let payload = null;
1885
+ if (this.browserEmitter && typeof this.browserEmitter.readClipboardPayload === "function") {
1886
+ payload = await this.browserEmitter.readClipboardPayload();
1887
+ } else {
1888
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
1889
+ payload = await this.collectClipboardFromPage(activePage);
1890
+ }
1891
+
1892
+ if (this.hasClipboardPayload(payload)) {
1893
+ this.logger.info("[BVTRecorder] Remote clipboard payload ready", {
1894
+ hasText: !!payload.text,
1895
+ hasHtml: !!payload.html,
1896
+ files: payload.files?.length ?? 0,
1897
+ });
1898
+ this.sendEvent(this.events.clipboardPush, {
1899
+ data: payload,
1900
+ trigger: message?.trigger ?? "copy",
1901
+ origin: message?.source ?? "browserUI",
1902
+ });
1903
+ return payload;
1904
+ }
1905
+
1906
+ this.logger.warn("[BVTRecorder] Remote clipboard payload empty or unavailable");
1907
+ this.sendEvent(this.events.clipboardError, {
1908
+ message: "Remote clipboard is empty",
1909
+ trigger: message?.trigger ?? "copy",
1910
+ });
1911
+ return null;
1912
+ } catch (error) {
1913
+ this.logger.error("[BVTRecorder] Error reading clipboard payload", error);
1914
+ this.sendEvent(this.events.clipboardError, {
1915
+ message: "Failed to read clipboard contents from the remote session",
1916
+ trigger: message?.trigger ?? "copy",
1917
+ details: error instanceof Error ? error.message : String(error),
1918
+ });
1919
+ throw error;
1920
+ }
1921
+ }
1922
+
1923
+ async injectClipboardIntoPage(page, payload) {
1924
+ if (!page) {
1925
+ return;
1926
+ }
1927
+
1928
+ try {
1929
+ await page
1930
+ .context()
1931
+ .grantPermissions(["clipboard-read", "clipboard-write"])
1932
+ .catch(() => {});
1933
+ await page.evaluate(async (clipboardPayload) => {
1934
+ const toArrayBuffer = (base64) => {
1935
+ if (!base64) {
1936
+ return null;
1937
+ }
1938
+ const binaryString = atob(base64);
1939
+ const len = binaryString.length;
1940
+ const bytes = new Uint8Array(len);
1941
+ for (let i = 0; i < len; i += 1) {
1942
+ bytes[i] = binaryString.charCodeAt(i);
1943
+ }
1944
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
1945
+ };
1946
+
1947
+ const createFileFromPayload = (filePayload) => {
1948
+ const buffer = toArrayBuffer(filePayload?.data);
1949
+ if (!buffer) {
1950
+ return null;
1951
+ }
1952
+ const name = filePayload?.name || "clipboard-file";
1953
+ const type = filePayload?.type || "application/octet-stream";
1954
+ const lastModified = filePayload?.lastModified ?? Date.now();
1955
+ try {
1956
+ return new File([buffer], name, { type, lastModified });
1957
+ } catch (error) {
1958
+ console.warn("Clipboard bridge could not recreate File object", error);
1959
+ return null;
1960
+ }
1961
+ };
1962
+
1963
+ let dataTransfer = null;
1964
+ try {
1965
+ dataTransfer = new DataTransfer();
1966
+ } catch (error) {
1967
+ console.warn("Clipboard bridge could not create DataTransfer", error);
1968
+ }
1969
+
1970
+ if (dataTransfer) {
1971
+ if (clipboardPayload?.text) {
1972
+ try {
1973
+ dataTransfer.setData("text/plain", clipboardPayload.text);
1974
+ } catch (error) {
1975
+ console.warn("Clipboard bridge failed to set text/plain", error);
1976
+ }
1977
+ }
1978
+ if (clipboardPayload?.html) {
1979
+ try {
1980
+ dataTransfer.setData("text/html", clipboardPayload.html);
1981
+ } catch (error) {
1982
+ console.warn("Clipboard bridge failed to set text/html", error);
1983
+ }
1984
+ }
1985
+ if (Array.isArray(clipboardPayload?.files)) {
1986
+ for (const filePayload of clipboardPayload.files) {
1987
+ const file = createFileFromPayload(filePayload);
1988
+ if (file) {
1989
+ try {
1990
+ dataTransfer.items.add(file);
1991
+ } catch (error) {
1992
+ console.warn("Clipboard bridge failed to append file", error);
1993
+ }
1994
+ }
1995
+ }
1996
+ }
1997
+ }
1998
+
1999
+ let target = document.activeElement || document.body;
2000
+ if (!target) {
2001
+ target = document.body || null;
2002
+ }
2003
+
2004
+ let pasteHandled = false;
2005
+ if (dataTransfer && target && typeof target.dispatchEvent === "function") {
2006
+ try {
2007
+ const clipboardEvent = new ClipboardEvent("paste", {
2008
+ clipboardData: dataTransfer,
2009
+ bubbles: true,
2010
+ cancelable: true,
2011
+ });
2012
+ pasteHandled = target.dispatchEvent(clipboardEvent);
2013
+ } catch (error) {
2014
+ console.warn("Clipboard bridge failed to dispatch synthetic paste event", error);
2015
+ }
2016
+ }
2017
+
2018
+ if (pasteHandled) {
2019
+ return;
2020
+ }
2021
+
2022
+ const callLegacyExecCommand = (command, value) => {
2023
+ const execCommand = document && document["execCommand"];
2024
+ if (typeof execCommand === "function") {
2025
+ try {
2026
+ return execCommand.call(document, command, false, value);
2027
+ } catch (error) {
2028
+ console.warn("Clipboard bridge failed to execute legacy command", error);
2029
+ }
2030
+ }
2031
+ return false;
2032
+ };
2033
+
2034
+ if (clipboardPayload?.html) {
2035
+ const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
2036
+ if (inserted) {
2037
+ return;
2038
+ }
2039
+ try {
2040
+ const selection = window.getSelection?.();
2041
+ if (selection && selection.rangeCount > 0) {
2042
+ const range = selection.getRangeAt(0);
2043
+ range.deleteContents();
2044
+ const fragment = range.createContextualFragment(clipboardPayload.html);
2045
+ range.insertNode(fragment);
2046
+ range.collapse(false);
2047
+ return;
2048
+ }
2049
+ } catch (error) {
2050
+ console.warn("Clipboard bridge could not insert HTML via Range APIs", error);
2051
+ }
2052
+ }
2053
+
2054
+ if (clipboardPayload?.text) {
2055
+ const inserted = callLegacyExecCommand("insertText", clipboardPayload.text);
2056
+ if (inserted) {
2057
+ return;
2058
+ }
2059
+ try {
2060
+ const selection = window.getSelection?.();
2061
+ if (selection && selection.rangeCount > 0) {
2062
+ const range = selection.getRangeAt(0);
2063
+ range.deleteContents();
2064
+ range.insertNode(document.createTextNode(clipboardPayload.text));
2065
+ range.collapse(false);
2066
+ return;
2067
+ }
2068
+ } catch (error) {
2069
+ console.warn("Clipboard bridge could not insert text via Range APIs", error);
2070
+ }
2071
+ }
2072
+
2073
+ if (clipboardPayload?.text && target && "value" in target) {
2074
+ try {
2075
+ const input = target;
2076
+ const start = input.selectionStart ?? input.value.length ?? 0;
2077
+ const end = input.selectionEnd ?? input.value.length ?? 0;
2078
+ const value = input.value ?? "";
2079
+ const text = clipboardPayload.text;
2080
+ input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
2081
+ const caret = start + text.length;
2082
+ if (typeof input.setSelectionRange === "function") {
2083
+ input.setSelectionRange(caret, caret);
2084
+ }
2085
+ input.dispatchEvent(new Event("input", { bubbles: true }));
2086
+ } catch (error) {
2087
+ console.warn("Clipboard bridge failed to mutate input element", error);
2088
+ }
2089
+ }
2090
+ }, payload);
2091
+ } catch (error) {
2092
+ throw error;
2093
+ }
2094
+ }
2095
+
2096
+ async createTab(url) {
2097
+ try {
2098
+ await this.browserEmitter?.createTab(url);
2099
+ } catch (error) {
2100
+ this.logger.error("Error creating tab:", error);
2101
+ this.sendEvent(this.events.browserStateError, {
2102
+ message: "Error creating tab",
2103
+ code: "CREATE_TAB_ERROR",
2104
+ });
2105
+ }
2106
+ }
2107
+
2108
+ async closeTab(pageId) {
2109
+ try {
2110
+ await this.browserEmitter?.closeTab(pageId);
2111
+ } catch (error) {
2112
+ this.logger.error("Error closing tab:", error);
2113
+ this.sendEvent(this.events.browserStateError, {
2114
+ message: "Error closing tab",
2115
+ code: "CLOSE_TAB_ERROR",
2116
+ });
2117
+ }
2118
+ }
2119
+
2120
+ async selectTab(pageId) {
2121
+ try {
2122
+ await this.browserEmitter?.selectTab(pageId);
2123
+ } catch (error) {
2124
+ this.logger.error("Error selecting tab:", error);
2125
+ this.sendEvent(this.events.browserStateError, {
2126
+ message: "Error selecting tab",
2127
+ code: "SELECT_TAB_ERROR",
2128
+ });
2129
+ }
2130
+ }
2131
+
2132
+ async navigateTab({ pageId, url }) {
2133
+ try {
2134
+ if (!pageId || !url) {
2135
+ this.logger.error("navigateTab called without pageId or url", { pageId, url });
2136
+ return;
2137
+ }
2138
+ await this.browserEmitter?.navigateTab(pageId, url);
2139
+ } catch (error) {
2140
+ this.logger.error("Error navigating tab:", error);
2141
+ this.sendEvent(this.events.browserStateError, {
2142
+ message: "Error navigating tab",
2143
+ code: "NAVIGATE_TAB_ERROR",
2144
+ });
2145
+ }
2146
+ }
2147
+
2148
+ async reloadTab(pageId) {
2149
+ try {
2150
+ await this.browserEmitter?.reloadTab(pageId);
2151
+ } catch (error) {
2152
+ this.logger.error("Error reloading tab:", error);
2153
+ this.sendEvent(this.events.browserStateError, {
2154
+ message: "Error reloading tab",
2155
+ code: "RELOAD_TAB_ERROR",
2156
+ });
2157
+ }
2158
+ }
2159
+
2160
+ async goBack(pageId) {
2161
+ try {
2162
+ await this.browserEmitter?.goBack(pageId);
2163
+ } catch (error) {
2164
+ this.logger.error("Error navigating back:", error);
2165
+ this.sendEvent(this.events.browserStateError, {
2166
+ message: "Error navigating back",
2167
+ code: "GO_BACK_ERROR",
2168
+ });
2169
+ }
2170
+ }
2171
+
2172
+ async goForward(pageId) {
2173
+ try {
2174
+ await this.browserEmitter?.goForward(pageId);
2175
+ } catch (error) {
2176
+ this.logger.error("Error navigating forward:", error);
2177
+ this.sendEvent(this.events.browserStateError, {
2178
+ message: "Error navigating forward",
2179
+ code: "GO_FORWARD_ERROR",
2180
+ });
2181
+ }
2182
+ }
2183
+ }