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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +73 -73
  2. package/bin/assets/scripts/recorder.js +87 -49
  3. package/bin/assets/scripts/snapshot_capturer.js +10 -17
  4. package/bin/assets/scripts/unique_locators.js +169 -47
  5. package/bin/assets/templates/_hooks_template.txt +6 -2
  6. package/bin/assets/templates/utils_template.txt +16 -16
  7. package/bin/client/code_cleanup/utils.js +16 -7
  8. package/bin/client/code_gen/code_inversion.js +115 -0
  9. package/bin/client/code_gen/duplication_analysis.js +2 -1
  10. package/bin/client/code_gen/function_signature.js +4 -0
  11. package/bin/client/code_gen/page_reflection.js +52 -11
  12. package/bin/client/code_gen/playwright_codeget.js +164 -75
  13. package/bin/client/cucumber/feature.js +4 -17
  14. package/bin/client/cucumber/steps_definitions.js +13 -0
  15. package/bin/client/local_agent.js +1 -0
  16. package/bin/client/recorderv3/bvt_init.js +305 -0
  17. package/bin/client/recorderv3/bvt_recorder.js +1031 -61
  18. package/bin/client/recorderv3/implemented_steps.js +2 -0
  19. package/bin/client/recorderv3/index.js +3 -286
  20. package/bin/client/recorderv3/services.js +818 -142
  21. package/bin/client/recorderv3/step_runner.js +21 -4
  22. package/bin/client/recorderv3/step_utils.js +194 -118
  23. package/bin/client/recorderv3/update_feature.js +87 -39
  24. package/bin/client/recorderv3/wbr_entry.js +61 -0
  25. package/bin/client/recording.js +1 -0
  26. package/bin/client/upload-service.js +2 -0
  27. package/bin/client/utils/app_dir.js +21 -0
  28. package/bin/client/utils/socket_logger.js +87 -125
  29. package/bin/index.js +4 -1
  30. package/package.json +11 -5
  31. package/bin/client/recorderv3/app_dir.js +0 -23
  32. package/bin/client/recorderv3/network.js +0 -299
  33. package/bin/client/recorderv3/scriptTest.js +0 -5
  34. package/bin/client/recorderv3/ws_server.js +0 -72
@@ -1,24 +1,333 @@
1
1
  // define the jsdoc type for the input
2
2
  import { closeContext, initContext, _getDataFile, resetTestData } from "automation_model";
3
- import { existsSync, readdirSync, readFileSync, rmSync } from "fs";
4
- import path from "path";
5
- import url from "url";
3
+ import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
4
+ import path from "node:path";
5
+ import url from "node:url";
6
6
  import { getImplementedSteps, parseRouteFiles } from "./implemented_steps.js";
7
- import { NamesService } from "./services.js";
7
+ import { NamesService, PublishService } from "./services.js";
8
8
  import { BVTStepRunner } from "./step_runner.js";
9
- import { readFile, writeFile } from "fs/promises";
10
- import { updateStepDefinitions, loadStepDefinitions, getCommandsForImplementedStep } from "./step_utils.js";
11
- import { updateFeatureFile } from "./update_feature.js";
9
+ import { readFile, writeFile } from "node:fs/promises";
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
14
  import { unEscapeNonPrintables } from "../cucumber/utils.js";
16
15
  import { findAvailablePort } from "../utils/index.js";
17
- import socketLogger from "../utils/socket_logger.js";
16
+ import socketLogger, { getErrorMessage } from "../utils/socket_logger.js";
18
17
  import { tmpdir } from "os";
18
+ import { faker } from "@faker-js/faker/locale/en_US";
19
+ import { chromium } from "playwright-core";
20
+
19
21
  const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
20
22
 
21
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
+
22
331
  export function getInitScript(config, options) {
23
332
  const preScript = `
24
333
  window.__bvt_Recorder_config = ${JSON.stringify(config ?? null)};
@@ -28,7 +337,7 @@ export function getInitScript(config, options) {
28
337
  path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"),
29
338
  "utf8"
30
339
  );
31
- return preScript + recorderScript;
340
+ return preScript + recorderScript + clipboardBridgeScript;
32
341
  }
33
342
 
34
343
  async function evaluate(frame, script) {
@@ -42,7 +351,6 @@ async function evaluate(frame, script) {
42
351
  }
43
352
 
44
353
  async function findNestedFrameSelector(frame, obj) {
45
- console.log("Testing new version");
46
354
  try {
47
355
  const parent = frame.parentFrame();
48
356
  if (!parent) return { children: obj };
@@ -53,8 +361,7 @@ async function findNestedFrameSelector(frame, obj) {
53
361
  }, frameElement);
54
362
  return findNestedFrameSelector(parent, { children: obj, selectors });
55
363
  } catch (e) {
56
- socketLogger.error(`Error in findNestedFrameSelector: ${e}`);
57
- console.error(e);
364
+ socketLogger.error(`Error in script evaluation: ${getErrorMessage(e)}`, undefined, "findNestedFrameSelector");
58
365
  }
59
366
  }
60
367
  const transformFillAction = (action, el) => {
@@ -156,6 +463,17 @@ const transformAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode,
156
463
  }
157
464
  }
158
465
  };
466
+ const diffPaths = (currentPath, newPath) => {
467
+ const currentDomain = new URL(currentPath).hostname;
468
+ const newDomain = new URL(newPath).hostname;
469
+ if (currentDomain !== newDomain) {
470
+ return true;
471
+ } else {
472
+ const currentRoute = new URL(currentPath).pathname;
473
+ const newRoute = new URL(newPath).pathname;
474
+ return currentRoute !== newRoute;
475
+ }
476
+ };
159
477
  /**
160
478
  * @typedef {Object} BVTRecorderInput
161
479
  * @property {string} envName
@@ -185,12 +503,16 @@ export class BVTRecorder {
185
503
  projectDir: this.projectDir,
186
504
  logger: this.logger,
187
505
  });
506
+ this.workspaceService = new PublishService(this.TOKEN);
188
507
  this.pageSet = new Set();
189
508
  this.lastKnownUrlPath = "";
190
- this.world = { attach: () => {} };
509
+ this.world = { attach: () => { } };
191
510
  this.shouldTakeScreenshot = true;
192
511
  this.watcher = null;
193
512
  this.networkEventsFolder = path.join(tmpdir(), "blinq_network_events");
513
+ this.tempProjectFolder = `${tmpdir()}/bvt_temp_project_${Math.floor(Math.random() * 1000000)}`;
514
+ this.tempSnapshotsFolder = path.join(this.tempProjectFolder, "data/snapshots");
515
+
194
516
  if (existsSync(this.networkEventsFolder)) {
195
517
  rmSync(this.networkEventsFolder, { recursive: true, force: true });
196
518
  }
@@ -208,6 +530,12 @@ export class BVTRecorder {
208
530
  cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
209
531
  cmdExecutionError: "BVTRecorder.cmdExecutionError",
210
532
  interceptResults: "BVTRecorder.interceptResults",
533
+ onDebugURLChange: "BVTRecorder.onDebugURLChange",
534
+ updateCommand: "BVTRecorder.updateCommand",
535
+ browserStateSync: "BrowserService.stateSync",
536
+ browserStateError: "BrowserService.stateError",
537
+ clipboardPush: "BrowserService.clipboardPush",
538
+ clipboardError: "BrowserService.clipboardError",
211
539
  };
212
540
  bindings = {
213
541
  __bvt_recordCommand: async ({ frame, page, context }, event) => {
@@ -237,6 +565,25 @@ export class BVTRecorder {
237
565
  __bvt_getObject: (_src, obj) => {
238
566
  this.processObject(obj);
239
567
  },
568
+ __bvt_reportClipboard: async ({ page }, payload) => {
569
+ try {
570
+ if (!payload) {
571
+ return;
572
+ }
573
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
574
+ if (activePage && activePage !== page) {
575
+ return;
576
+ }
577
+ const pageUrl = typeof page?.url === "function" ? page.url() : null;
578
+ this.sendEvent(this.events.clipboardPush, {
579
+ data: payload,
580
+ trigger: payload?.trigger ?? "copy",
581
+ pageUrl,
582
+ });
583
+ } catch (error) {
584
+ this.logger.error("Error forwarding clipboard payload from page", error);
585
+ }
586
+ },
240
587
  };
241
588
 
242
589
  getSnapshot = async (attr) => {
@@ -294,11 +641,15 @@ export class BVTRecorder {
294
641
  }
295
642
 
296
643
  async _initBrowser({ url }) {
297
- this.#remoteDebuggerPort = await findAvailablePort();
298
- process.env.CDP_LISTEN_PORT = this.#remoteDebuggerPort;
644
+ if (process.env.CDP_LISTEN_PORT === undefined) {
645
+ this.#remoteDebuggerPort = await findAvailablePort();
646
+ process.env.CDP_LISTEN_PORT = this.#remoteDebuggerPort;
647
+ } else {
648
+ this.#remoteDebuggerPort = process.env.CDP_LISTEN_PORT;
649
+ }
299
650
 
300
651
  // this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
301
- this.world = { attach: () => {} };
652
+ this.world = { attach: () => { } };
302
653
 
303
654
  const ai_config_file = path.join(this.projectDir, "ai_config.json");
304
655
  let ai_config = {};
@@ -318,7 +669,8 @@ export class BVTRecorder {
318
669
  ],
319
670
  };
320
671
 
321
- const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName);
672
+ const scenario = { pickle: this.scenarioDoc };
673
+ const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName, scenario);
322
674
  this.bvtContext = bvtContext;
323
675
  this.stepRunner = new BVTStepRunner({
324
676
  projectDir: this.projectDir,
@@ -344,8 +696,7 @@ export class BVTRecorder {
344
696
  },
345
697
  bvtContext: this.bvtContext,
346
698
  });
347
- const context = bvtContext.playContext;
348
- this.context = context;
699
+ this.context = bvtContext.playContext;
349
700
  this.web = bvtContext.stable || bvtContext.web;
350
701
  this.web.tryAllStrategies = true;
351
702
  this.page = bvtContext.page;
@@ -359,9 +710,19 @@ export class BVTRecorder {
359
710
  await this.context.exposeBinding(name, handler);
360
711
  }
361
712
  this._watchTestData();
362
- this.web.onRestoreSaveState = (url) => {
363
- this._initBrowser({ url });
713
+ this.web.onRestoreSaveState = async (url) => {
714
+ await this._initBrowser({ url });
715
+ this._addPagelisteners(this.context);
716
+ this._addFrameNavigateListener(this.page);
364
717
  };
718
+
719
+ // create a second browser for locator generation
720
+ this.backgroundBrowser = await chromium.launch({
721
+ headless: true,
722
+ });
723
+ this.backgroundContext = await this.backgroundBrowser.newContext({});
724
+ await this.backgroundContext.addInitScript({ content: this.getInitScripts(this.config) });
725
+ await this.backgroundContext.newPage();
365
726
  }
366
727
  async onClosePopup() {
367
728
  // console.log("close popups");
@@ -403,13 +764,14 @@ export class BVTRecorder {
403
764
 
404
765
  await this.page.goto(url, {
405
766
  waitUntil: "domcontentloaded",
767
+ timeout: this.config.page_timeout ?? 60_000,
406
768
  });
407
769
  // add listener for frame navigation on current tab
408
770
  this._addFrameNavigateListener(this.page);
409
771
 
410
772
  // eval init script on current tab
411
773
  // await this._initPage(this.page);
412
- this.#currentURL = new URL(url).pathname;
774
+ this.#currentURL = url;
413
775
 
414
776
  await this.page.dispatchEvent("html", "scroll");
415
777
  await delay(1000);
@@ -451,14 +813,15 @@ export class BVTRecorder {
451
813
  element: { inputID: "frame" },
452
814
  });
453
815
 
454
- const newPath = new URL(frame.url()).pathname;
816
+ const newUrl = frame.url();
817
+ const newPath = new URL(newUrl).pathname;
455
818
  const newTitle = await frame.title();
456
- if (newPath !== this.#currentURL) {
819
+ const changed = diffPaths(this.#currentURL, newUrl);
820
+
821
+ if (changed) {
457
822
  this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
458
- this.#currentURL = newPath;
823
+ this.#currentURL = newUrl;
459
824
  }
460
- // await this._setRecordingMode(frame);
461
- // await this._initPage(page);
462
825
  } catch (error) {
463
826
  this.logger.error("Error in frame navigate event");
464
827
  this.logger.error(error);
@@ -623,7 +986,6 @@ export class BVTRecorder {
623
986
  try {
624
987
  if (page.isClosed()) return;
625
988
  this.pageSet.add(page);
626
-
627
989
  await page.waitForLoadState("domcontentloaded");
628
990
 
629
991
  // add listener for frame navigation on new tab
@@ -688,6 +1050,52 @@ export class BVTRecorder {
688
1050
  console.error("Error in saving screenshot: ", error);
689
1051
  }
690
1052
  }
1053
+ async generateLocators(event) {
1054
+ const snapshotDetails = event.snapshotDetails;
1055
+ if (!snapshotDetails) {
1056
+ throw new Error("No snapshot details found");
1057
+ }
1058
+ const mode = event.mode;
1059
+ const inputID = event.element.inputID;
1060
+
1061
+ const { id, contextId, doc } = snapshotDetails;
1062
+ // const selector = `[data-blinq-id="${id}"]`;
1063
+ const newPage = await this.backgroundContext.newPage();
1064
+ await newPage.setContent(doc, { waitUntil: "domcontentloaded" });
1065
+ const locatorsObj = await newPage.evaluate(
1066
+ ([id, contextId, mode]) => {
1067
+ const recorder = window.__bvt_Recorder;
1068
+ const contextElement = document.querySelector(`[data-blinq-context-id="${contextId}"]`);
1069
+ const el = document.querySelector(`[data-blinq-id="${id}"]`);
1070
+ if (contextElement) {
1071
+ const result = recorder.locatorGenerator.toContextLocators(el, contextElement);
1072
+ return result;
1073
+ }
1074
+ const isRecordingText = mode === "recordingText";
1075
+ return recorder.locatorGenerator.getElementLocators(el, {
1076
+ excludeText: isRecordingText,
1077
+ });
1078
+ },
1079
+ [id, contextId, mode]
1080
+ );
1081
+
1082
+ // console.log(`Generated locators: for ${inputID}: `, JSON.stringify(locatorsObj));
1083
+ await newPage.close();
1084
+ if (event.nestFrmLoc?.children) {
1085
+ locatorsObj.nestFrmLoc = event.nestFrmLoc.children;
1086
+ }
1087
+
1088
+ this.sendEvent(this.events.updateCommand, {
1089
+ locators: {
1090
+ locators: locatorsObj.locators,
1091
+ nestFrmLoc: locatorsObj.nestFrmLoc,
1092
+ iframe_src: !event.frame.isTop ? event.frame.url : undefined,
1093
+ },
1094
+ allStrategyLocators: locatorsObj.allStrategyLocators,
1095
+ inputID,
1096
+ });
1097
+ // const
1098
+ }
691
1099
  async onAction(event) {
692
1100
  this._updateUrlPath();
693
1101
  // const locators = this.overlayLocators(event);
@@ -701,25 +1109,41 @@ export class BVTRecorder {
701
1109
  event.mode === "recordingHover",
702
1110
  event.mode === "multiInspecting"
703
1111
  ),
704
- locators: {
705
- locators: event.locators,
706
- iframe_src: !event.frame.isTop ? event.frame.url : undefined,
707
- },
708
- allStrategyLocators: event.allStrategyLocators,
1112
+ // locators: {
1113
+ // locators: event.locators,
1114
+ // iframe_src: !event.frame.isTop ? event.frame.url : undefined,
1115
+ // },
1116
+ // allStrategyLocators: event.allStrategyLocators,
709
1117
  url: event.frame.url,
710
1118
  title: event.frame.title,
711
1119
  extract: {},
712
1120
  lastKnownUrlPath: this.lastKnownUrlPath,
713
1121
  };
714
- if (event.nestFrmLoc?.children) {
715
- cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
716
- }
1122
+ // if (event.nestFrmLoc?.children) {
1123
+ // cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
1124
+ // }
717
1125
  // this.logger.info({ event });
718
1126
  if (this.shouldTakeScreenshot) {
719
1127
  await this.storeScreenshot(event);
720
1128
  }
721
- this.sendEvent(this.events.onNewCommand, cmdEvent);
722
- this._updateUrlPath();
1129
+ // this.sendEvent(this.events.onNewCommand, cmdEvent);
1130
+ // this._updateUrlPath();
1131
+ if (event.locators) {
1132
+ Object.assign(cmdEvent, {
1133
+ locators: {
1134
+ locators: event.locators,
1135
+ iframe_src: !event.frame.isTop ? event.frame.url : undefined,
1136
+ nestFrmLoc: event.nestFrmLoc?.children,
1137
+ },
1138
+ allStrategyLocators: event.allStrategyLocators,
1139
+ })
1140
+ this.sendEvent(this.events.onNewCommand, cmdEvent);
1141
+ this._updateUrlPath();
1142
+ } else {
1143
+ this.sendEvent(this.events.onNewCommand, cmdEvent);
1144
+ this._updateUrlPath();
1145
+ await this.generateLocators(event);
1146
+ }
723
1147
  }
724
1148
  _updateUrlPath() {
725
1149
  try {
@@ -735,13 +1159,12 @@ export class BVTRecorder {
735
1159
  }
736
1160
  async closeBrowser() {
737
1161
  delete process.env.TEMP_RUN;
738
- await this.watcher.close().then(() => {});
1162
+ await this.watcher.close().then(() => { });
739
1163
  this.watcher = null;
740
1164
  this.previousIndex = null;
741
1165
  this.previousHistoryLength = null;
742
1166
  this.previousUrl = null;
743
1167
  this.previousEntries = null;
744
-
745
1168
  await closeContext();
746
1169
  this.pageSet.clear();
747
1170
  }
@@ -764,25 +1187,26 @@ export class BVTRecorder {
764
1187
  for (let i = 0; i < 3; i++) {
765
1188
  result = 0;
766
1189
  try {
767
- for (const page of this.context.pages()) {
768
- for (const frame of page.frames()) {
769
- try {
770
- //scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params: Params)
771
- const frameResult = await this.web._locateElementByText(
772
- frame,
773
- searchString,
774
- tag,
775
- regex,
776
- partial,
777
- ignoreCase,
778
- {}
779
- );
780
- result += frameResult.elementCount;
781
- } catch (e) {
782
- console.log(e);
783
- }
1190
+ // for (const page of this.context.pages()) {
1191
+ const page = this.web.page;
1192
+ for (const frame of page.frames()) {
1193
+ try {
1194
+ //scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params: Params)
1195
+ const frameResult = await this.web._locateElementByText(
1196
+ frame,
1197
+ searchString,
1198
+ tag,
1199
+ regex,
1200
+ partial,
1201
+ ignoreCase,
1202
+ {}
1203
+ );
1204
+ result += frameResult.elementCount;
1205
+ } catch (e) {
1206
+ console.log(e);
784
1207
  }
785
1208
  }
1209
+ // }
786
1210
 
787
1211
  return result;
788
1212
  } catch (e) {
@@ -835,13 +1259,19 @@ export class BVTRecorder {
835
1259
  }
836
1260
  async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
837
1261
  const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
1262
+
1263
+ const env = path.basename(this.envName, ".json");
838
1264
  const _env = {
839
1265
  TOKEN: this.TOKEN,
840
1266
  TEMP_RUN: true,
841
1267
  REPORT_FOLDER: this.bvtContext.reportFolder,
842
1268
  BLINQ_ENV: this.envName,
843
1269
  DEBUG: "blinq:route",
1270
+ // BVT_TEMP_SNAPSHOTS_FOLDER: step.isImplemented ? path.join(this.tempSnapshotsFolder, env) : undefined,
844
1271
  };
1272
+ if (!step.isImplemented) {
1273
+ _env.BVT_TEMP_SNAPSHOTS_FOLDER = path.join(this.tempSnapshotsFolder, env);
1274
+ }
845
1275
 
846
1276
  this.bvtContext.navigate = true;
847
1277
  this.bvtContext.loadedRoutes = null;
@@ -861,6 +1291,7 @@ export class BVTRecorder {
861
1291
  await this.setMode("running");
862
1292
 
863
1293
  try {
1294
+ step.text = step.text.trim();
864
1295
  const { result, info } = await this.stepRunner.runStep(
865
1296
  {
866
1297
  step,
@@ -887,10 +1318,22 @@ export class BVTRecorder {
887
1318
  this.bvtContext.navigate = false;
888
1319
  }
889
1320
  }
890
- async saveScenario({ scenario, featureName, override, isSingleStep }) {
891
- await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
892
- if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
893
- await this.cleanup({ tags: scenario.tags });
1321
+ async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env, AICode }) {
1322
+ const res = await this.workspaceService.saveScenario({
1323
+ scenario,
1324
+ featureName,
1325
+ override,
1326
+ isSingleStep,
1327
+ branch,
1328
+ isEditing,
1329
+ projectId: path.basename(this.projectDir),
1330
+ env: env ?? this.envName,
1331
+ });
1332
+ if (res.success) {
1333
+ await this.cleanup({ tags: scenario.tags });
1334
+ } else {
1335
+ throw new Error(res.message || "Error saving scenario");
1336
+ }
894
1337
  }
895
1338
  async getImplementedSteps() {
896
1339
  const stepsAndScenarios = await getImplementedSteps(this.projectDir);
@@ -1055,9 +1498,11 @@ export class BVTRecorder {
1055
1498
  const featureFilePath = path.join(this.projectDir, "features", featureName);
1056
1499
  const gherkinDoc = this.parseFeatureFile(featureFilePath);
1057
1500
  const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
1501
+ this.scenarioDoc = scenario;
1058
1502
 
1059
1503
  const steps = [];
1060
1504
  const parameters = [];
1505
+ const datasets = [];
1061
1506
  if (scenario.examples && scenario.examples.length > 0) {
1062
1507
  const example = scenario.examples[0];
1063
1508
  example?.tableHeader?.cells.forEach((cell, index) => {
@@ -1065,7 +1510,26 @@ export class BVTRecorder {
1065
1510
  key: cell.value,
1066
1511
  value: unEscapeNonPrintables(example.tableBody[0].cells[index].value),
1067
1512
  });
1513
+ // datasets.push({
1514
+ // data: example.tableBody[]
1515
+ // })
1068
1516
  });
1517
+
1518
+ for (let i = 0; i < example.tableBody.length; i++) {
1519
+ const row = example.tableBody[i];
1520
+ // for (const row of example.tableBody) {
1521
+ const paramters = [];
1522
+ row.cells.forEach((cell, index) => {
1523
+ paramters.push({
1524
+ key: example.tableHeader.cells[index].value,
1525
+ value: unEscapeNonPrintables(cell.value),
1526
+ });
1527
+ });
1528
+ datasets.push({
1529
+ data: paramters,
1530
+ datasetId: i,
1531
+ });
1532
+ }
1069
1533
  }
1070
1534
 
1071
1535
  for (const step of scenario.steps) {
@@ -1086,6 +1550,7 @@ export class BVTRecorder {
1086
1550
  tags: scenario.tags.map((tag) => tag.name),
1087
1551
  steps,
1088
1552
  parameters,
1553
+ datasets,
1089
1554
  };
1090
1555
  }
1091
1556
  async findRelatedTextInAllFrames({ searchString, climb, contextText, params }) {
@@ -1235,4 +1700,509 @@ export class BVTRecorder {
1235
1700
  this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
1236
1701
  }
1237
1702
  }
1703
+
1704
+ async fakeParams(params) {
1705
+ const newFakeParams = {};
1706
+ Object.keys(params).forEach((key) => {
1707
+ if (!params[key].startsWith("{{") || !params[key].endsWith("}}")) {
1708
+ newFakeParams[key] = params[key];
1709
+ return;
1710
+ }
1711
+
1712
+ try {
1713
+ const value = params[key].substring(2, params[key].length - 2).trim();
1714
+ const faking = value.split("(")[0].split(".");
1715
+ let argument = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
1716
+ argument = isNaN(Number(argument)) || argument === "" ? argument : Number(argument);
1717
+ let fakeFunc = faker;
1718
+ faking.forEach((f) => {
1719
+ fakeFunc = fakeFunc[f];
1720
+ });
1721
+ const newValue = fakeFunc(argument);
1722
+ newFakeParams[key] = newValue;
1723
+ } catch (error) {
1724
+ newFakeParams[key] = params[key];
1725
+ }
1726
+ });
1727
+
1728
+ return newFakeParams;
1729
+ }
1730
+
1731
+ async getBrowserState() {
1732
+ try {
1733
+ const state = await this.browserEmitter?.getState();
1734
+ this.sendEvent(this.events.browserStateSync, state);
1735
+ } catch (error) {
1736
+ this.logger.error("Error getting browser state:", error);
1737
+ this.sendEvent(this.events.browserStateError, {
1738
+ message: "Error getting browser state",
1739
+ code: "GET_STATE_ERROR",
1740
+ });
1741
+ }
1742
+ }
1743
+
1744
+ async applyClipboardPayload(message) {
1745
+ const payload = message?.data ?? message;
1746
+
1747
+ this.logger.info("[BVTRecorder] applyClipboardPayload called", {
1748
+ hasPayload: !!payload,
1749
+ hasText: !!payload?.text,
1750
+ hasHtml: !!payload?.html,
1751
+ trigger: message?.trigger,
1752
+ });
1753
+
1754
+ if (!payload) {
1755
+ this.logger.warn("[BVTRecorder] No payload provided");
1756
+ return;
1757
+ }
1758
+
1759
+ try {
1760
+ if (this.browserEmitter && typeof this.browserEmitter.applyClipboardPayload === "function") {
1761
+ this.logger.info("[BVTRecorder] Using RemoteBrowserService to apply clipboard");
1762
+ await this.browserEmitter.applyClipboardPayload(payload);
1763
+ return;
1764
+ }
1765
+
1766
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
1767
+ if (!activePage) {
1768
+ this.logger.warn("[BVTRecorder] No active page available");
1769
+ return;
1770
+ }
1771
+
1772
+ this.logger.info("[BVTRecorder] Applying clipboard to page", {
1773
+ url: activePage.url(),
1774
+ isClosed: activePage.isClosed(),
1775
+ });
1776
+
1777
+ const result = await activePage.evaluate((clipboardData) => {
1778
+ console.log("[Page] Executing clipboard application", clipboardData);
1779
+ if (typeof window.__bvt_applyClipboardData === "function") {
1780
+ return window.__bvt_applyClipboardData(clipboardData);
1781
+ }
1782
+ console.error("[Page] __bvt_applyClipboardData function not found!");
1783
+ return false;
1784
+ }, payload);
1785
+
1786
+ this.logger.info("[BVTRecorder] Clipboard application result:", result);
1787
+
1788
+ if (!result) {
1789
+ this.logger.warn("[BVTRecorder] Clipboard data not applied successfully");
1790
+ } else {
1791
+ this.logger.info("[BVTRecorder] Clipboard data applied successfully");
1792
+ }
1793
+ } catch (error) {
1794
+ this.logger.error("[BVTRecorder] Error applying clipboard payload", error);
1795
+ this.sendEvent(this.events.clipboardError, {
1796
+ message: "Failed to apply clipboard contents to the remote session",
1797
+ trigger: message?.trigger ?? "paste",
1798
+ });
1799
+ }
1800
+ }
1801
+
1802
+ hasClipboardPayload(payload) {
1803
+ return Boolean(
1804
+ payload && (payload.text || payload.html || (Array.isArray(payload.files) && payload.files.length > 0))
1805
+ );
1806
+ }
1807
+
1808
+ async collectClipboardFromPage(page) {
1809
+ if (!page) {
1810
+ this.logger.warn("[BVTRecorder] No page available to collect clipboard data");
1811
+ return null;
1812
+ }
1813
+ try {
1814
+ await page
1815
+ .context()
1816
+ .grantPermissions(["clipboard-read", "clipboard-write"])
1817
+ .catch((error) => {
1818
+ this.logger.warn("[BVTRecorder] Failed to grant clipboard permissions before read", error);
1819
+ });
1820
+
1821
+ const payload = await page.evaluate(async () => {
1822
+ const result = {};
1823
+ if (typeof navigator === "undefined" || !navigator.clipboard) {
1824
+ return result;
1825
+ }
1826
+
1827
+ const arrayBufferToBase64 = (buffer) => {
1828
+ let binary = "";
1829
+ const bytes = new Uint8Array(buffer);
1830
+ const chunkSize = 0x8000;
1831
+ for (let index = 0; index < bytes.length; index += chunkSize) {
1832
+ const chunk = bytes.subarray(index, index + chunkSize);
1833
+ binary += String.fromCharCode(...chunk);
1834
+ }
1835
+ return btoa(binary);
1836
+ };
1837
+
1838
+ const files = [];
1839
+
1840
+ if (typeof navigator.clipboard.read === "function") {
1841
+ try {
1842
+ const items = await navigator.clipboard.read();
1843
+ for (const item of items) {
1844
+ if (item.types.includes("text/html") && !result.html) {
1845
+ const blob = await item.getType("text/html");
1846
+ result.html = await blob.text();
1847
+ }
1848
+ if (item.types.includes("text/plain") && !result.text) {
1849
+ const blob = await item.getType("text/plain");
1850
+ result.text = await blob.text();
1851
+ }
1852
+ for (const type of item.types) {
1853
+ if (type.startsWith("text/")) {
1854
+ continue;
1855
+ }
1856
+ try {
1857
+ const blob = await item.getType(type);
1858
+ const buffer = await blob.arrayBuffer();
1859
+ files.push({
1860
+ name: `clipboard-file-${files.length + 1}`,
1861
+ type,
1862
+ lastModified: Date.now(),
1863
+ data: arrayBufferToBase64(buffer),
1864
+ });
1865
+ } catch (error) {
1866
+ console.warn("[BVTRecorder] Failed to serialize clipboard blob", { type, error });
1867
+ }
1868
+ }
1869
+ }
1870
+ } catch (error) {
1871
+ console.warn("[BVTRecorder] navigator.clipboard.read failed", error);
1872
+ }
1873
+ }
1874
+
1875
+ if (!result.text && typeof navigator.clipboard.readText === "function") {
1876
+ try {
1877
+ const text = await navigator.clipboard.readText();
1878
+ if (text) {
1879
+ result.text = text;
1880
+ }
1881
+ } catch (error) {
1882
+ console.warn("[BVTRecorder] navigator.clipboard.readText failed", error);
1883
+ }
1884
+ }
1885
+
1886
+ if (!result.text) {
1887
+ const selection = window.getSelection?.()?.toString?.();
1888
+ if (selection) {
1889
+ result.text = selection;
1890
+ }
1891
+ }
1892
+
1893
+ if (files.length > 0) {
1894
+ result.files = files;
1895
+ }
1896
+
1897
+ return result;
1898
+ });
1899
+
1900
+ return payload;
1901
+ } catch (error) {
1902
+ this.logger.error("[BVTRecorder] Error collecting clipboard payload", error);
1903
+ return null;
1904
+ }
1905
+ }
1906
+
1907
+ async readClipboardPayload(message) {
1908
+ try {
1909
+ let payload = null;
1910
+ if (this.browserEmitter && typeof this.browserEmitter.readClipboardPayload === "function") {
1911
+ payload = await this.browserEmitter.readClipboardPayload();
1912
+ } else {
1913
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
1914
+ payload = await this.collectClipboardFromPage(activePage);
1915
+ }
1916
+
1917
+ if (this.hasClipboardPayload(payload)) {
1918
+ this.logger.info("[BVTRecorder] Remote clipboard payload ready", {
1919
+ hasText: !!payload.text,
1920
+ hasHtml: !!payload.html,
1921
+ files: payload.files?.length ?? 0,
1922
+ });
1923
+ this.sendEvent(this.events.clipboardPush, {
1924
+ data: payload,
1925
+ trigger: message?.trigger ?? "copy",
1926
+ origin: message?.source ?? "browserUI",
1927
+ });
1928
+ return payload;
1929
+ }
1930
+
1931
+ this.logger.warn("[BVTRecorder] Remote clipboard payload empty or unavailable");
1932
+ this.sendEvent(this.events.clipboardError, {
1933
+ message: "Remote clipboard is empty",
1934
+ trigger: message?.trigger ?? "copy",
1935
+ });
1936
+ return null;
1937
+ } catch (error) {
1938
+ this.logger.error("[BVTRecorder] Error reading clipboard payload", error);
1939
+ this.sendEvent(this.events.clipboardError, {
1940
+ message: "Failed to read clipboard contents from the remote session",
1941
+ trigger: message?.trigger ?? "copy",
1942
+ details: error instanceof Error ? error.message : String(error),
1943
+ });
1944
+ throw error;
1945
+ }
1946
+ }
1947
+
1948
+ async injectClipboardIntoPage(page, payload) {
1949
+ if (!page) {
1950
+ return;
1951
+ }
1952
+
1953
+ try {
1954
+ await page
1955
+ .context()
1956
+ .grantPermissions(["clipboard-read", "clipboard-write"])
1957
+ .catch(() => { });
1958
+ await page.evaluate(async (clipboardPayload) => {
1959
+ const toArrayBuffer = (base64) => {
1960
+ if (!base64) {
1961
+ return null;
1962
+ }
1963
+ const binaryString = atob(base64);
1964
+ const len = binaryString.length;
1965
+ const bytes = new Uint8Array(len);
1966
+ for (let i = 0; i < len; i += 1) {
1967
+ bytes[i] = binaryString.charCodeAt(i);
1968
+ }
1969
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
1970
+ };
1971
+
1972
+ const createFileFromPayload = (filePayload) => {
1973
+ const buffer = toArrayBuffer(filePayload?.data);
1974
+ if (!buffer) {
1975
+ return null;
1976
+ }
1977
+ const name = filePayload?.name || "clipboard-file";
1978
+ const type = filePayload?.type || "application/octet-stream";
1979
+ const lastModified = filePayload?.lastModified ?? Date.now();
1980
+ try {
1981
+ return new File([buffer], name, { type, lastModified });
1982
+ } catch (error) {
1983
+ console.warn("Clipboard bridge could not recreate File object", error);
1984
+ return null;
1985
+ }
1986
+ };
1987
+
1988
+ let dataTransfer = null;
1989
+ try {
1990
+ dataTransfer = new DataTransfer();
1991
+ } catch (error) {
1992
+ console.warn("Clipboard bridge could not create DataTransfer", error);
1993
+ }
1994
+
1995
+ if (dataTransfer) {
1996
+ if (clipboardPayload?.text) {
1997
+ try {
1998
+ dataTransfer.setData("text/plain", clipboardPayload.text);
1999
+ } catch (error) {
2000
+ console.warn("Clipboard bridge failed to set text/plain", error);
2001
+ }
2002
+ }
2003
+ if (clipboardPayload?.html) {
2004
+ try {
2005
+ dataTransfer.setData("text/html", clipboardPayload.html);
2006
+ } catch (error) {
2007
+ console.warn("Clipboard bridge failed to set text/html", error);
2008
+ }
2009
+ }
2010
+ if (Array.isArray(clipboardPayload?.files)) {
2011
+ for (const filePayload of clipboardPayload.files) {
2012
+ const file = createFileFromPayload(filePayload);
2013
+ if (file) {
2014
+ try {
2015
+ dataTransfer.items.add(file);
2016
+ } catch (error) {
2017
+ console.warn("Clipboard bridge failed to append file", error);
2018
+ }
2019
+ }
2020
+ }
2021
+ }
2022
+ }
2023
+
2024
+ let target = document.activeElement || document.body;
2025
+ if (!target) {
2026
+ target = document.body || null;
2027
+ }
2028
+
2029
+ let pasteHandled = false;
2030
+ if (dataTransfer && target && typeof target.dispatchEvent === "function") {
2031
+ try {
2032
+ const clipboardEvent = new ClipboardEvent("paste", {
2033
+ clipboardData: dataTransfer,
2034
+ bubbles: true,
2035
+ cancelable: true,
2036
+ });
2037
+ pasteHandled = target.dispatchEvent(clipboardEvent);
2038
+ } catch (error) {
2039
+ console.warn("Clipboard bridge failed to dispatch synthetic paste event", error);
2040
+ }
2041
+ }
2042
+
2043
+ if (pasteHandled) {
2044
+ return;
2045
+ }
2046
+
2047
+ const callLegacyExecCommand = (command, value) => {
2048
+ const execCommand = document && document["execCommand"];
2049
+ if (typeof execCommand === "function") {
2050
+ try {
2051
+ return execCommand.call(document, command, false, value);
2052
+ } catch (error) {
2053
+ console.warn("Clipboard bridge failed to execute legacy command", error);
2054
+ }
2055
+ }
2056
+ return false;
2057
+ };
2058
+
2059
+ if (clipboardPayload?.html) {
2060
+ const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
2061
+ if (inserted) {
2062
+ return;
2063
+ }
2064
+ try {
2065
+ const selection = window.getSelection?.();
2066
+ if (selection && selection.rangeCount > 0) {
2067
+ const range = selection.getRangeAt(0);
2068
+ range.deleteContents();
2069
+ const fragment = range.createContextualFragment(clipboardPayload.html);
2070
+ range.insertNode(fragment);
2071
+ range.collapse(false);
2072
+ return;
2073
+ }
2074
+ } catch (error) {
2075
+ console.warn("Clipboard bridge could not insert HTML via Range APIs", error);
2076
+ }
2077
+ }
2078
+
2079
+ if (clipboardPayload?.text) {
2080
+ const inserted = callLegacyExecCommand("insertText", clipboardPayload.text);
2081
+ if (inserted) {
2082
+ return;
2083
+ }
2084
+ try {
2085
+ const selection = window.getSelection?.();
2086
+ if (selection && selection.rangeCount > 0) {
2087
+ const range = selection.getRangeAt(0);
2088
+ range.deleteContents();
2089
+ range.insertNode(document.createTextNode(clipboardPayload.text));
2090
+ range.collapse(false);
2091
+ return;
2092
+ }
2093
+ } catch (error) {
2094
+ console.warn("Clipboard bridge could not insert text via Range APIs", error);
2095
+ }
2096
+ }
2097
+
2098
+ if (clipboardPayload?.text && target && "value" in target) {
2099
+ try {
2100
+ const input = target;
2101
+ const start = input.selectionStart ?? input.value.length ?? 0;
2102
+ const end = input.selectionEnd ?? input.value.length ?? 0;
2103
+ const value = input.value ?? "";
2104
+ const text = clipboardPayload.text;
2105
+ input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
2106
+ const caret = start + text.length;
2107
+ if (typeof input.setSelectionRange === "function") {
2108
+ input.setSelectionRange(caret, caret);
2109
+ }
2110
+ input.dispatchEvent(new Event("input", { bubbles: true }));
2111
+ } catch (error) {
2112
+ console.warn("Clipboard bridge failed to mutate input element", error);
2113
+ }
2114
+ }
2115
+ }, payload);
2116
+ } catch (error) {
2117
+ throw error;
2118
+ }
2119
+ }
2120
+
2121
+ async createTab(url) {
2122
+ try {
2123
+ await this.browserEmitter?.createTab(url);
2124
+ } catch (error) {
2125
+ this.logger.error("Error creating tab:", error);
2126
+ this.sendEvent(this.events.browserStateError, {
2127
+ message: "Error creating tab",
2128
+ code: "CREATE_TAB_ERROR",
2129
+ });
2130
+ }
2131
+ }
2132
+
2133
+ async closeTab(pageId) {
2134
+ try {
2135
+ await this.browserEmitter?.closeTab(pageId);
2136
+ } catch (error) {
2137
+ this.logger.error("Error closing tab:", error);
2138
+ this.sendEvent(this.events.browserStateError, {
2139
+ message: "Error closing tab",
2140
+ code: "CLOSE_TAB_ERROR",
2141
+ });
2142
+ }
2143
+ }
2144
+
2145
+ async selectTab(pageId) {
2146
+ try {
2147
+ await this.browserEmitter?.selectTab(pageId);
2148
+ } catch (error) {
2149
+ this.logger.error("Error selecting tab:", error);
2150
+ this.sendEvent(this.events.browserStateError, {
2151
+ message: "Error selecting tab",
2152
+ code: "SELECT_TAB_ERROR",
2153
+ });
2154
+ }
2155
+ }
2156
+
2157
+ async navigateTab({ pageId, url }) {
2158
+ try {
2159
+ if (!pageId || !url) {
2160
+ this.logger.error("navigateTab called without pageId or url", { pageId, url });
2161
+ return;
2162
+ }
2163
+ await this.browserEmitter?.navigateTab(pageId, url);
2164
+ } catch (error) {
2165
+ this.logger.error("Error navigating tab:", error);
2166
+ this.sendEvent(this.events.browserStateError, {
2167
+ message: "Error navigating tab",
2168
+ code: "NAVIGATE_TAB_ERROR",
2169
+ });
2170
+ }
2171
+ }
2172
+
2173
+ async reloadTab(pageId) {
2174
+ try {
2175
+ await this.browserEmitter?.reloadTab(pageId);
2176
+ } catch (error) {
2177
+ this.logger.error("Error reloading tab:", error);
2178
+ this.sendEvent(this.events.browserStateError, {
2179
+ message: "Error reloading tab",
2180
+ code: "RELOAD_TAB_ERROR",
2181
+ });
2182
+ }
2183
+ }
2184
+
2185
+ async goBack(pageId) {
2186
+ try {
2187
+ await this.browserEmitter?.goBack(pageId);
2188
+ } catch (error) {
2189
+ this.logger.error("Error navigating back:", error);
2190
+ this.sendEvent(this.events.browserStateError, {
2191
+ message: "Error navigating back",
2192
+ code: "GO_BACK_ERROR",
2193
+ });
2194
+ }
2195
+ }
2196
+
2197
+ async goForward(pageId) {
2198
+ try {
2199
+ await this.browserEmitter?.goForward(pageId);
2200
+ } catch (error) {
2201
+ this.logger.error("Error navigating forward:", error);
2202
+ this.sendEvent(this.events.browserStateError, {
2203
+ message: "Error navigating forward",
2204
+ code: "GO_FORWARD_ERROR",
2205
+ });
2206
+ }
2207
+ }
1238
2208
  }