@dev-blinq/cucumber_client 1.0.1451-dev → 1.0.1451-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 +73 -73
  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 -48
  12. package/bin/assets/scripts/snapshot_capturer.js +147 -147
  13. package/bin/assets/scripts/unique_locators.js +170 -49
  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 +16 -7
  19. package/bin/client/code_gen/api_codegen.js +2 -2
  20. package/bin/client/code_gen/code_inversion.js +119 -2
  21. package/bin/client/code_gen/duplication_analysis.js +2 -1
  22. package/bin/client/code_gen/function_signature.js +4 -0
  23. package/bin/client/code_gen/page_reflection.js +52 -11
  24. package/bin/client/code_gen/playwright_codeget.js +163 -75
  25. package/bin/client/cucumber/feature.js +4 -17
  26. package/bin/client/cucumber/feature_data.js +2 -2
  27. package/bin/client/cucumber/project_to_document.js +8 -2
  28. package/bin/client/cucumber/steps_definitions.js +19 -3
  29. package/bin/client/local_agent.js +1 -0
  30. package/bin/client/parse_feature_file.js +23 -26
  31. package/bin/client/playground/projects/env.json +2 -2
  32. package/bin/client/recorderv3/bvt_init.js +305 -0
  33. package/bin/client/recorderv3/bvt_recorder.js +1011 -55
  34. package/bin/client/recorderv3/implemented_steps.js +2 -0
  35. package/bin/client/recorderv3/index.js +3 -286
  36. package/bin/client/recorderv3/services.js +818 -142
  37. package/bin/client/recorderv3/step_runner.js +25 -8
  38. package/bin/client/recorderv3/step_utils.js +569 -75
  39. package/bin/client/recorderv3/update_feature.js +87 -39
  40. package/bin/client/recorderv3/wbr_entry.js +61 -0
  41. package/bin/client/recording.js +1 -0
  42. package/bin/client/upload-service.js +4 -2
  43. package/bin/client/utils/app_dir.js +21 -0
  44. package/bin/client/utils/socket_logger.js +87 -125
  45. package/bin/index.js +4 -1
  46. package/package.json +11 -5
  47. package/bin/client/recorderv3/app_dir.js +0 -23
  48. package/bin/client/recorderv3/network.js +0 -299
  49. package/bin/client/recorderv3/scriptTest.js +0 -5
  50. 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) {
@@ -52,8 +361,7 @@ async function findNestedFrameSelector(frame, obj) {
52
361
  }, frameElement);
53
362
  return findNestedFrameSelector(parent, { children: obj, selectors });
54
363
  } catch (e) {
55
- socketLogger.error(`Error in findNestedFrameSelector: ${e}`);
56
- console.error(e);
364
+ socketLogger.error(`Error in script evaluation: ${getErrorMessage(e)}`, undefined, "findNestedFrameSelector");
57
365
  }
58
366
  }
59
367
  const transformFillAction = (action, el) => {
@@ -155,6 +463,17 @@ const transformAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode,
155
463
  }
156
464
  }
157
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
+ };
158
477
  /**
159
478
  * @typedef {Object} BVTRecorderInput
160
479
  * @property {string} envName
@@ -184,12 +503,16 @@ export class BVTRecorder {
184
503
  projectDir: this.projectDir,
185
504
  logger: this.logger,
186
505
  });
506
+ this.workspaceService = new PublishService(this.TOKEN);
187
507
  this.pageSet = new Set();
188
508
  this.lastKnownUrlPath = "";
189
509
  this.world = { attach: () => {} };
190
510
  this.shouldTakeScreenshot = true;
191
511
  this.watcher = null;
192
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
+
193
516
  if (existsSync(this.networkEventsFolder)) {
194
517
  rmSync(this.networkEventsFolder, { recursive: true, force: true });
195
518
  }
@@ -207,6 +530,12 @@ export class BVTRecorder {
207
530
  cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
208
531
  cmdExecutionError: "BVTRecorder.cmdExecutionError",
209
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",
210
539
  };
211
540
  bindings = {
212
541
  __bvt_recordCommand: async ({ frame, page, context }, event) => {
@@ -236,6 +565,25 @@ export class BVTRecorder {
236
565
  __bvt_getObject: (_src, obj) => {
237
566
  this.processObject(obj);
238
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
+ },
239
587
  };
240
588
 
241
589
  getSnapshot = async (attr) => {
@@ -293,8 +641,12 @@ export class BVTRecorder {
293
641
  }
294
642
 
295
643
  async _initBrowser({ url }) {
296
- this.#remoteDebuggerPort = await findAvailablePort();
297
- 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
+ }
298
650
 
299
651
  // this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
300
652
  this.world = { attach: () => {} };
@@ -317,7 +669,8 @@ export class BVTRecorder {
317
669
  ],
318
670
  };
319
671
 
320
- 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);
321
674
  this.bvtContext = bvtContext;
322
675
  this.stepRunner = new BVTStepRunner({
323
676
  projectDir: this.projectDir,
@@ -343,8 +696,7 @@ export class BVTRecorder {
343
696
  },
344
697
  bvtContext: this.bvtContext,
345
698
  });
346
- const context = bvtContext.playContext;
347
- this.context = context;
699
+ this.context = bvtContext.playContext;
348
700
  this.web = bvtContext.stable || bvtContext.web;
349
701
  this.web.tryAllStrategies = true;
350
702
  this.page = bvtContext.page;
@@ -358,9 +710,19 @@ export class BVTRecorder {
358
710
  await this.context.exposeBinding(name, handler);
359
711
  }
360
712
  this._watchTestData();
361
- this.web.onRestoreSaveState = (url) => {
362
- this._initBrowser({ url });
713
+ this.web.onRestoreSaveState = async (url) => {
714
+ await this._initBrowser({ url });
715
+ this._addPagelisteners(this.context);
716
+ this._addFrameNavigateListener(this.page);
363
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();
364
726
  }
365
727
  async onClosePopup() {
366
728
  // console.log("close popups");
@@ -402,13 +764,14 @@ export class BVTRecorder {
402
764
 
403
765
  await this.page.goto(url, {
404
766
  waitUntil: "domcontentloaded",
767
+ timeout: this.config.page_timeout ?? 60_000,
405
768
  });
406
769
  // add listener for frame navigation on current tab
407
770
  this._addFrameNavigateListener(this.page);
408
771
 
409
772
  // eval init script on current tab
410
773
  // await this._initPage(this.page);
411
- this.#currentURL = new URL(url).pathname;
774
+ this.#currentURL = url;
412
775
 
413
776
  await this.page.dispatchEvent("html", "scroll");
414
777
  await delay(1000);
@@ -450,14 +813,15 @@ export class BVTRecorder {
450
813
  element: { inputID: "frame" },
451
814
  });
452
815
 
453
- const newPath = new URL(frame.url()).pathname;
816
+ const newUrl = frame.url();
817
+ const newPath = new URL(newUrl).pathname;
454
818
  const newTitle = await frame.title();
455
- if (newPath !== this.#currentURL) {
819
+ const changed = diffPaths(this.#currentURL, newUrl);
820
+
821
+ if (changed) {
456
822
  this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
457
- this.#currentURL = newPath;
823
+ this.#currentURL = newUrl;
458
824
  }
459
- // await this._setRecordingMode(frame);
460
- // await this._initPage(page);
461
825
  } catch (error) {
462
826
  this.logger.error("Error in frame navigate event");
463
827
  this.logger.error(error);
@@ -622,7 +986,6 @@ export class BVTRecorder {
622
986
  try {
623
987
  if (page.isClosed()) return;
624
988
  this.pageSet.add(page);
625
-
626
989
  await page.waitForLoadState("domcontentloaded");
627
990
 
628
991
  // add listener for frame navigation on new tab
@@ -687,6 +1050,52 @@ export class BVTRecorder {
687
1050
  console.error("Error in saving screenshot: ", error);
688
1051
  }
689
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
+ }
690
1099
  async onAction(event) {
691
1100
  this._updateUrlPath();
692
1101
  // const locators = this.overlayLocators(event);
@@ -700,25 +1109,26 @@ export class BVTRecorder {
700
1109
  event.mode === "recordingHover",
701
1110
  event.mode === "multiInspecting"
702
1111
  ),
703
- locators: {
704
- locators: event.locators,
705
- iframe_src: !event.frame.isTop ? event.frame.url : undefined,
706
- },
707
- allStrategyLocators: event.allStrategyLocators,
1112
+ // locators: {
1113
+ // locators: event.locators,
1114
+ // iframe_src: !event.frame.isTop ? event.frame.url : undefined,
1115
+ // },
1116
+ // allStrategyLocators: event.allStrategyLocators,
708
1117
  url: event.frame.url,
709
1118
  title: event.frame.title,
710
1119
  extract: {},
711
1120
  lastKnownUrlPath: this.lastKnownUrlPath,
712
1121
  };
713
- if (event.nestFrmLoc?.children) {
714
- cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
715
- }
1122
+ // if (event.nestFrmLoc?.children) {
1123
+ // cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
1124
+ // }
716
1125
  // this.logger.info({ event });
717
1126
  if (this.shouldTakeScreenshot) {
718
1127
  await this.storeScreenshot(event);
719
1128
  }
720
1129
  this.sendEvent(this.events.onNewCommand, cmdEvent);
721
1130
  this._updateUrlPath();
1131
+ await this.generateLocators(event);
722
1132
  }
723
1133
  _updateUrlPath() {
724
1134
  try {
@@ -740,7 +1150,6 @@ export class BVTRecorder {
740
1150
  this.previousHistoryLength = null;
741
1151
  this.previousUrl = null;
742
1152
  this.previousEntries = null;
743
-
744
1153
  await closeContext();
745
1154
  this.pageSet.clear();
746
1155
  }
@@ -763,25 +1172,26 @@ export class BVTRecorder {
763
1172
  for (let i = 0; i < 3; i++) {
764
1173
  result = 0;
765
1174
  try {
766
- for (const page of this.context.pages()) {
767
- for (const frame of page.frames()) {
768
- try {
769
- //scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params: Params)
770
- const frameResult = await this.web._locateElementByText(
771
- frame,
772
- searchString,
773
- tag,
774
- regex,
775
- partial,
776
- ignoreCase,
777
- {}
778
- );
779
- result += frameResult.elementCount;
780
- } catch (e) {
781
- console.log(e);
782
- }
1175
+ // for (const page of this.context.pages()) {
1176
+ const page = this.web.page;
1177
+ for (const frame of page.frames()) {
1178
+ try {
1179
+ //scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params: Params)
1180
+ const frameResult = await this.web._locateElementByText(
1181
+ frame,
1182
+ searchString,
1183
+ tag,
1184
+ regex,
1185
+ partial,
1186
+ ignoreCase,
1187
+ {}
1188
+ );
1189
+ result += frameResult.elementCount;
1190
+ } catch (e) {
1191
+ console.log(e);
783
1192
  }
784
1193
  }
1194
+ // }
785
1195
 
786
1196
  return result;
787
1197
  } catch (e) {
@@ -834,13 +1244,19 @@ export class BVTRecorder {
834
1244
  }
835
1245
  async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
836
1246
  const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
1247
+
1248
+ const env = path.basename(this.envName, ".json");
837
1249
  const _env = {
838
1250
  TOKEN: this.TOKEN,
839
1251
  TEMP_RUN: true,
840
1252
  REPORT_FOLDER: this.bvtContext.reportFolder,
841
1253
  BLINQ_ENV: this.envName,
842
1254
  DEBUG: "blinq:route",
1255
+ // BVT_TEMP_SNAPSHOTS_FOLDER: step.isImplemented ? path.join(this.tempSnapshotsFolder, env) : undefined,
843
1256
  };
1257
+ if (!step.isImplemented) {
1258
+ _env.BVT_TEMP_SNAPSHOTS_FOLDER = path.join(this.tempSnapshotsFolder, env);
1259
+ }
844
1260
 
845
1261
  this.bvtContext.navigate = true;
846
1262
  this.bvtContext.loadedRoutes = null;
@@ -860,6 +1276,7 @@ export class BVTRecorder {
860
1276
  await this.setMode("running");
861
1277
 
862
1278
  try {
1279
+ step.text = step.text.trim();
863
1280
  const { result, info } = await this.stepRunner.runStep(
864
1281
  {
865
1282
  step,
@@ -886,10 +1303,22 @@ export class BVTRecorder {
886
1303
  this.bvtContext.navigate = false;
887
1304
  }
888
1305
  }
889
- async saveScenario({ scenario, featureName, override, isSingleStep }) {
890
- await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
891
- if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
892
- await this.cleanup({ tags: scenario.tags });
1306
+ async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env, AICode }) {
1307
+ const res = await this.workspaceService.saveScenario({
1308
+ scenario,
1309
+ featureName,
1310
+ override,
1311
+ isSingleStep,
1312
+ branch,
1313
+ isEditing,
1314
+ projectId: path.basename(this.projectDir),
1315
+ env: env ?? this.envName,
1316
+ });
1317
+ if (res.success) {
1318
+ await this.cleanup({ tags: scenario.tags });
1319
+ } else {
1320
+ throw new Error(res.message || "Error saving scenario");
1321
+ }
893
1322
  }
894
1323
  async getImplementedSteps() {
895
1324
  const stepsAndScenarios = await getImplementedSteps(this.projectDir);
@@ -1054,9 +1483,11 @@ export class BVTRecorder {
1054
1483
  const featureFilePath = path.join(this.projectDir, "features", featureName);
1055
1484
  const gherkinDoc = this.parseFeatureFile(featureFilePath);
1056
1485
  const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
1486
+ this.scenarioDoc = scenario;
1057
1487
 
1058
1488
  const steps = [];
1059
1489
  const parameters = [];
1490
+ const datasets = [];
1060
1491
  if (scenario.examples && scenario.examples.length > 0) {
1061
1492
  const example = scenario.examples[0];
1062
1493
  example?.tableHeader?.cells.forEach((cell, index) => {
@@ -1064,7 +1495,26 @@ export class BVTRecorder {
1064
1495
  key: cell.value,
1065
1496
  value: unEscapeNonPrintables(example.tableBody[0].cells[index].value),
1066
1497
  });
1498
+ // datasets.push({
1499
+ // data: example.tableBody[]
1500
+ // })
1067
1501
  });
1502
+
1503
+ for (let i = 0; i < example.tableBody.length; i++) {
1504
+ const row = example.tableBody[i];
1505
+ // for (const row of example.tableBody) {
1506
+ const paramters = [];
1507
+ row.cells.forEach((cell, index) => {
1508
+ paramters.push({
1509
+ key: example.tableHeader.cells[index].value,
1510
+ value: unEscapeNonPrintables(cell.value),
1511
+ });
1512
+ });
1513
+ datasets.push({
1514
+ data: paramters,
1515
+ datasetId: i,
1516
+ });
1517
+ }
1068
1518
  }
1069
1519
 
1070
1520
  for (const step of scenario.steps) {
@@ -1085,6 +1535,7 @@ export class BVTRecorder {
1085
1535
  tags: scenario.tags.map((tag) => tag.name),
1086
1536
  steps,
1087
1537
  parameters,
1538
+ datasets,
1088
1539
  };
1089
1540
  }
1090
1541
  async findRelatedTextInAllFrames({ searchString, climb, contextText, params }) {
@@ -1234,4 +1685,509 @@ export class BVTRecorder {
1234
1685
  this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
1235
1686
  }
1236
1687
  }
1688
+
1689
+ async fakeParams(params) {
1690
+ const newFakeParams = {};
1691
+ Object.keys(params).forEach((key) => {
1692
+ if (!params[key].startsWith("{{") || !params[key].endsWith("}}")) {
1693
+ newFakeParams[key] = params[key];
1694
+ return;
1695
+ }
1696
+
1697
+ try {
1698
+ const value = params[key].substring(2, params[key].length - 2).trim();
1699
+ const faking = value.split("(")[0].split(".");
1700
+ let argument = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
1701
+ argument = isNaN(Number(argument)) || argument === "" ? argument : Number(argument);
1702
+ let fakeFunc = faker;
1703
+ faking.forEach((f) => {
1704
+ fakeFunc = fakeFunc[f];
1705
+ });
1706
+ const newValue = fakeFunc(argument);
1707
+ newFakeParams[key] = newValue;
1708
+ } catch (error) {
1709
+ newFakeParams[key] = params[key];
1710
+ }
1711
+ });
1712
+
1713
+ return newFakeParams;
1714
+ }
1715
+
1716
+ async getBrowserState() {
1717
+ try {
1718
+ const state = await this.browserEmitter?.getState();
1719
+ this.sendEvent(this.events.browserStateSync, state);
1720
+ } catch (error) {
1721
+ this.logger.error("Error getting browser state:", error);
1722
+ this.sendEvent(this.events.browserStateError, {
1723
+ message: "Error getting browser state",
1724
+ code: "GET_STATE_ERROR",
1725
+ });
1726
+ }
1727
+ }
1728
+
1729
+ async applyClipboardPayload(message) {
1730
+ const payload = message?.data ?? message;
1731
+
1732
+ this.logger.info("[BVTRecorder] applyClipboardPayload called", {
1733
+ hasPayload: !!payload,
1734
+ hasText: !!payload?.text,
1735
+ hasHtml: !!payload?.html,
1736
+ trigger: message?.trigger,
1737
+ });
1738
+
1739
+ if (!payload) {
1740
+ this.logger.warn("[BVTRecorder] No payload provided");
1741
+ return;
1742
+ }
1743
+
1744
+ try {
1745
+ if (this.browserEmitter && typeof this.browserEmitter.applyClipboardPayload === "function") {
1746
+ this.logger.info("[BVTRecorder] Using RemoteBrowserService to apply clipboard");
1747
+ await this.browserEmitter.applyClipboardPayload(payload);
1748
+ return;
1749
+ }
1750
+
1751
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
1752
+ if (!activePage) {
1753
+ this.logger.warn("[BVTRecorder] No active page available");
1754
+ return;
1755
+ }
1756
+
1757
+ this.logger.info("[BVTRecorder] Applying clipboard to page", {
1758
+ url: activePage.url(),
1759
+ isClosed: activePage.isClosed(),
1760
+ });
1761
+
1762
+ const result = await activePage.evaluate((clipboardData) => {
1763
+ console.log("[Page] Executing clipboard application", clipboardData);
1764
+ if (typeof window.__bvt_applyClipboardData === "function") {
1765
+ return window.__bvt_applyClipboardData(clipboardData);
1766
+ }
1767
+ console.error("[Page] __bvt_applyClipboardData function not found!");
1768
+ return false;
1769
+ }, payload);
1770
+
1771
+ this.logger.info("[BVTRecorder] Clipboard application result:", result);
1772
+
1773
+ if (!result) {
1774
+ this.logger.warn("[BVTRecorder] Clipboard data not applied successfully");
1775
+ } else {
1776
+ this.logger.info("[BVTRecorder] Clipboard data applied successfully");
1777
+ }
1778
+ } catch (error) {
1779
+ this.logger.error("[BVTRecorder] Error applying clipboard payload", error);
1780
+ this.sendEvent(this.events.clipboardError, {
1781
+ message: "Failed to apply clipboard contents to the remote session",
1782
+ trigger: message?.trigger ?? "paste",
1783
+ });
1784
+ }
1785
+ }
1786
+
1787
+ hasClipboardPayload(payload) {
1788
+ return Boolean(
1789
+ payload && (payload.text || payload.html || (Array.isArray(payload.files) && payload.files.length > 0))
1790
+ );
1791
+ }
1792
+
1793
+ async collectClipboardFromPage(page) {
1794
+ if (!page) {
1795
+ this.logger.warn("[BVTRecorder] No page available to collect clipboard data");
1796
+ return null;
1797
+ }
1798
+ try {
1799
+ await page
1800
+ .context()
1801
+ .grantPermissions(["clipboard-read", "clipboard-write"])
1802
+ .catch((error) => {
1803
+ this.logger.warn("[BVTRecorder] Failed to grant clipboard permissions before read", error);
1804
+ });
1805
+
1806
+ const payload = await page.evaluate(async () => {
1807
+ const result = {};
1808
+ if (typeof navigator === "undefined" || !navigator.clipboard) {
1809
+ return result;
1810
+ }
1811
+
1812
+ const arrayBufferToBase64 = (buffer) => {
1813
+ let binary = "";
1814
+ const bytes = new Uint8Array(buffer);
1815
+ const chunkSize = 0x8000;
1816
+ for (let index = 0; index < bytes.length; index += chunkSize) {
1817
+ const chunk = bytes.subarray(index, index + chunkSize);
1818
+ binary += String.fromCharCode(...chunk);
1819
+ }
1820
+ return btoa(binary);
1821
+ };
1822
+
1823
+ const files = [];
1824
+
1825
+ if (typeof navigator.clipboard.read === "function") {
1826
+ try {
1827
+ const items = await navigator.clipboard.read();
1828
+ for (const item of items) {
1829
+ if (item.types.includes("text/html") && !result.html) {
1830
+ const blob = await item.getType("text/html");
1831
+ result.html = await blob.text();
1832
+ }
1833
+ if (item.types.includes("text/plain") && !result.text) {
1834
+ const blob = await item.getType("text/plain");
1835
+ result.text = await blob.text();
1836
+ }
1837
+ for (const type of item.types) {
1838
+ if (type.startsWith("text/")) {
1839
+ continue;
1840
+ }
1841
+ try {
1842
+ const blob = await item.getType(type);
1843
+ const buffer = await blob.arrayBuffer();
1844
+ files.push({
1845
+ name: `clipboard-file-${files.length + 1}`,
1846
+ type,
1847
+ lastModified: Date.now(),
1848
+ data: arrayBufferToBase64(buffer),
1849
+ });
1850
+ } catch (error) {
1851
+ console.warn("[BVTRecorder] Failed to serialize clipboard blob", { type, error });
1852
+ }
1853
+ }
1854
+ }
1855
+ } catch (error) {
1856
+ console.warn("[BVTRecorder] navigator.clipboard.read failed", error);
1857
+ }
1858
+ }
1859
+
1860
+ if (!result.text && typeof navigator.clipboard.readText === "function") {
1861
+ try {
1862
+ const text = await navigator.clipboard.readText();
1863
+ if (text) {
1864
+ result.text = text;
1865
+ }
1866
+ } catch (error) {
1867
+ console.warn("[BVTRecorder] navigator.clipboard.readText failed", error);
1868
+ }
1869
+ }
1870
+
1871
+ if (!result.text) {
1872
+ const selection = window.getSelection?.()?.toString?.();
1873
+ if (selection) {
1874
+ result.text = selection;
1875
+ }
1876
+ }
1877
+
1878
+ if (files.length > 0) {
1879
+ result.files = files;
1880
+ }
1881
+
1882
+ return result;
1883
+ });
1884
+
1885
+ return payload;
1886
+ } catch (error) {
1887
+ this.logger.error("[BVTRecorder] Error collecting clipboard payload", error);
1888
+ return null;
1889
+ }
1890
+ }
1891
+
1892
+ async readClipboardPayload(message) {
1893
+ try {
1894
+ let payload = null;
1895
+ if (this.browserEmitter && typeof this.browserEmitter.readClipboardPayload === "function") {
1896
+ payload = await this.browserEmitter.readClipboardPayload();
1897
+ } else {
1898
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
1899
+ payload = await this.collectClipboardFromPage(activePage);
1900
+ }
1901
+
1902
+ if (this.hasClipboardPayload(payload)) {
1903
+ this.logger.info("[BVTRecorder] Remote clipboard payload ready", {
1904
+ hasText: !!payload.text,
1905
+ hasHtml: !!payload.html,
1906
+ files: payload.files?.length ?? 0,
1907
+ });
1908
+ this.sendEvent(this.events.clipboardPush, {
1909
+ data: payload,
1910
+ trigger: message?.trigger ?? "copy",
1911
+ origin: message?.source ?? "browserUI",
1912
+ });
1913
+ return payload;
1914
+ }
1915
+
1916
+ this.logger.warn("[BVTRecorder] Remote clipboard payload empty or unavailable");
1917
+ this.sendEvent(this.events.clipboardError, {
1918
+ message: "Remote clipboard is empty",
1919
+ trigger: message?.trigger ?? "copy",
1920
+ });
1921
+ return null;
1922
+ } catch (error) {
1923
+ this.logger.error("[BVTRecorder] Error reading clipboard payload", error);
1924
+ this.sendEvent(this.events.clipboardError, {
1925
+ message: "Failed to read clipboard contents from the remote session",
1926
+ trigger: message?.trigger ?? "copy",
1927
+ details: error instanceof Error ? error.message : String(error),
1928
+ });
1929
+ throw error;
1930
+ }
1931
+ }
1932
+
1933
+ async injectClipboardIntoPage(page, payload) {
1934
+ if (!page) {
1935
+ return;
1936
+ }
1937
+
1938
+ try {
1939
+ await page
1940
+ .context()
1941
+ .grantPermissions(["clipboard-read", "clipboard-write"])
1942
+ .catch(() => {});
1943
+ await page.evaluate(async (clipboardPayload) => {
1944
+ const toArrayBuffer = (base64) => {
1945
+ if (!base64) {
1946
+ return null;
1947
+ }
1948
+ const binaryString = atob(base64);
1949
+ const len = binaryString.length;
1950
+ const bytes = new Uint8Array(len);
1951
+ for (let i = 0; i < len; i += 1) {
1952
+ bytes[i] = binaryString.charCodeAt(i);
1953
+ }
1954
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
1955
+ };
1956
+
1957
+ const createFileFromPayload = (filePayload) => {
1958
+ const buffer = toArrayBuffer(filePayload?.data);
1959
+ if (!buffer) {
1960
+ return null;
1961
+ }
1962
+ const name = filePayload?.name || "clipboard-file";
1963
+ const type = filePayload?.type || "application/octet-stream";
1964
+ const lastModified = filePayload?.lastModified ?? Date.now();
1965
+ try {
1966
+ return new File([buffer], name, { type, lastModified });
1967
+ } catch (error) {
1968
+ console.warn("Clipboard bridge could not recreate File object", error);
1969
+ return null;
1970
+ }
1971
+ };
1972
+
1973
+ let dataTransfer = null;
1974
+ try {
1975
+ dataTransfer = new DataTransfer();
1976
+ } catch (error) {
1977
+ console.warn("Clipboard bridge could not create DataTransfer", error);
1978
+ }
1979
+
1980
+ if (dataTransfer) {
1981
+ if (clipboardPayload?.text) {
1982
+ try {
1983
+ dataTransfer.setData("text/plain", clipboardPayload.text);
1984
+ } catch (error) {
1985
+ console.warn("Clipboard bridge failed to set text/plain", error);
1986
+ }
1987
+ }
1988
+ if (clipboardPayload?.html) {
1989
+ try {
1990
+ dataTransfer.setData("text/html", clipboardPayload.html);
1991
+ } catch (error) {
1992
+ console.warn("Clipboard bridge failed to set text/html", error);
1993
+ }
1994
+ }
1995
+ if (Array.isArray(clipboardPayload?.files)) {
1996
+ for (const filePayload of clipboardPayload.files) {
1997
+ const file = createFileFromPayload(filePayload);
1998
+ if (file) {
1999
+ try {
2000
+ dataTransfer.items.add(file);
2001
+ } catch (error) {
2002
+ console.warn("Clipboard bridge failed to append file", error);
2003
+ }
2004
+ }
2005
+ }
2006
+ }
2007
+ }
2008
+
2009
+ let target = document.activeElement || document.body;
2010
+ if (!target) {
2011
+ target = document.body || null;
2012
+ }
2013
+
2014
+ let pasteHandled = false;
2015
+ if (dataTransfer && target && typeof target.dispatchEvent === "function") {
2016
+ try {
2017
+ const clipboardEvent = new ClipboardEvent("paste", {
2018
+ clipboardData: dataTransfer,
2019
+ bubbles: true,
2020
+ cancelable: true,
2021
+ });
2022
+ pasteHandled = target.dispatchEvent(clipboardEvent);
2023
+ } catch (error) {
2024
+ console.warn("Clipboard bridge failed to dispatch synthetic paste event", error);
2025
+ }
2026
+ }
2027
+
2028
+ if (pasteHandled) {
2029
+ return;
2030
+ }
2031
+
2032
+ const callLegacyExecCommand = (command, value) => {
2033
+ const execCommand = document && document["execCommand"];
2034
+ if (typeof execCommand === "function") {
2035
+ try {
2036
+ return execCommand.call(document, command, false, value);
2037
+ } catch (error) {
2038
+ console.warn("Clipboard bridge failed to execute legacy command", error);
2039
+ }
2040
+ }
2041
+ return false;
2042
+ };
2043
+
2044
+ if (clipboardPayload?.html) {
2045
+ const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
2046
+ if (inserted) {
2047
+ return;
2048
+ }
2049
+ try {
2050
+ const selection = window.getSelection?.();
2051
+ if (selection && selection.rangeCount > 0) {
2052
+ const range = selection.getRangeAt(0);
2053
+ range.deleteContents();
2054
+ const fragment = range.createContextualFragment(clipboardPayload.html);
2055
+ range.insertNode(fragment);
2056
+ range.collapse(false);
2057
+ return;
2058
+ }
2059
+ } catch (error) {
2060
+ console.warn("Clipboard bridge could not insert HTML via Range APIs", error);
2061
+ }
2062
+ }
2063
+
2064
+ if (clipboardPayload?.text) {
2065
+ const inserted = callLegacyExecCommand("insertText", clipboardPayload.text);
2066
+ if (inserted) {
2067
+ return;
2068
+ }
2069
+ try {
2070
+ const selection = window.getSelection?.();
2071
+ if (selection && selection.rangeCount > 0) {
2072
+ const range = selection.getRangeAt(0);
2073
+ range.deleteContents();
2074
+ range.insertNode(document.createTextNode(clipboardPayload.text));
2075
+ range.collapse(false);
2076
+ return;
2077
+ }
2078
+ } catch (error) {
2079
+ console.warn("Clipboard bridge could not insert text via Range APIs", error);
2080
+ }
2081
+ }
2082
+
2083
+ if (clipboardPayload?.text && target && "value" in target) {
2084
+ try {
2085
+ const input = target;
2086
+ const start = input.selectionStart ?? input.value.length ?? 0;
2087
+ const end = input.selectionEnd ?? input.value.length ?? 0;
2088
+ const value = input.value ?? "";
2089
+ const text = clipboardPayload.text;
2090
+ input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
2091
+ const caret = start + text.length;
2092
+ if (typeof input.setSelectionRange === "function") {
2093
+ input.setSelectionRange(caret, caret);
2094
+ }
2095
+ input.dispatchEvent(new Event("input", { bubbles: true }));
2096
+ } catch (error) {
2097
+ console.warn("Clipboard bridge failed to mutate input element", error);
2098
+ }
2099
+ }
2100
+ }, payload);
2101
+ } catch (error) {
2102
+ throw error;
2103
+ }
2104
+ }
2105
+
2106
+ async createTab(url) {
2107
+ try {
2108
+ await this.browserEmitter?.createTab(url);
2109
+ } catch (error) {
2110
+ this.logger.error("Error creating tab:", error);
2111
+ this.sendEvent(this.events.browserStateError, {
2112
+ message: "Error creating tab",
2113
+ code: "CREATE_TAB_ERROR",
2114
+ });
2115
+ }
2116
+ }
2117
+
2118
+ async closeTab(pageId) {
2119
+ try {
2120
+ await this.browserEmitter?.closeTab(pageId);
2121
+ } catch (error) {
2122
+ this.logger.error("Error closing tab:", error);
2123
+ this.sendEvent(this.events.browserStateError, {
2124
+ message: "Error closing tab",
2125
+ code: "CLOSE_TAB_ERROR",
2126
+ });
2127
+ }
2128
+ }
2129
+
2130
+ async selectTab(pageId) {
2131
+ try {
2132
+ await this.browserEmitter?.selectTab(pageId);
2133
+ } catch (error) {
2134
+ this.logger.error("Error selecting tab:", error);
2135
+ this.sendEvent(this.events.browserStateError, {
2136
+ message: "Error selecting tab",
2137
+ code: "SELECT_TAB_ERROR",
2138
+ });
2139
+ }
2140
+ }
2141
+
2142
+ async navigateTab({ pageId, url }) {
2143
+ try {
2144
+ if (!pageId || !url) {
2145
+ this.logger.error("navigateTab called without pageId or url", { pageId, url });
2146
+ return;
2147
+ }
2148
+ await this.browserEmitter?.navigateTab(pageId, url);
2149
+ } catch (error) {
2150
+ this.logger.error("Error navigating tab:", error);
2151
+ this.sendEvent(this.events.browserStateError, {
2152
+ message: "Error navigating tab",
2153
+ code: "NAVIGATE_TAB_ERROR",
2154
+ });
2155
+ }
2156
+ }
2157
+
2158
+ async reloadTab(pageId) {
2159
+ try {
2160
+ await this.browserEmitter?.reloadTab(pageId);
2161
+ } catch (error) {
2162
+ this.logger.error("Error reloading tab:", error);
2163
+ this.sendEvent(this.events.browserStateError, {
2164
+ message: "Error reloading tab",
2165
+ code: "RELOAD_TAB_ERROR",
2166
+ });
2167
+ }
2168
+ }
2169
+
2170
+ async goBack(pageId) {
2171
+ try {
2172
+ await this.browserEmitter?.goBack(pageId);
2173
+ } catch (error) {
2174
+ this.logger.error("Error navigating back:", error);
2175
+ this.sendEvent(this.events.browserStateError, {
2176
+ message: "Error navigating back",
2177
+ code: "GO_BACK_ERROR",
2178
+ });
2179
+ }
2180
+ }
2181
+
2182
+ async goForward(pageId) {
2183
+ try {
2184
+ await this.browserEmitter?.goForward(pageId);
2185
+ } catch (error) {
2186
+ this.logger.error("Error navigating forward:", error);
2187
+ this.sendEvent(this.events.browserStateError, {
2188
+ message: "Error navigating forward",
2189
+ code: "GO_FORWARD_ERROR",
2190
+ });
2191
+ }
2192
+ }
1237
2193
  }