@dev-blinq/cucumber_client 1.0.1612-dev → 1.0.1614-dev

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.
@@ -311,6 +311,26 @@ async function BVTRecorderInit({ envName, projectDir, roomId, TOKEN, socket = nu
311
311
  console.log("Received browserUI.selectTab", input);
312
312
  await recorder.selectTab(input);
313
313
  },
314
+ "browserUI.navigateTab": async (input) => {
315
+ console.log("Received browserUI.navigateTab", input);
316
+ await recorder.navigateTab(input);
317
+ },
318
+ "browserUI.reloadTab": async (input) => {
319
+ console.log("Received browserUI.reloadTab", input);
320
+ await recorder.reloadTab(input);
321
+ },
322
+ "browserUI.goBack": async (input) => {
323
+ console.log("Received browserUI.goBack", input);
324
+ await recorder.goBack(input);
325
+ },
326
+ "browserUI.goForward": async (input) => {
327
+ console.log("Received browserUI.goForward", input);
328
+ await recorder.goForward(input);
329
+ },
330
+ "browserUI.clipboardWrite": async (input) => {
331
+ console.log("Received browserUI.clipboardWrite");
332
+ await recorder.applyClipboardPayload(input);
333
+ },
314
334
  "recorderWindow.cleanup": async (input) => {
315
335
  return recorder.cleanup(input);
316
336
  },
@@ -30,7 +30,139 @@ export function getInitScript(config, options) {
30
30
  path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"),
31
31
  "utf8"
32
32
  );
33
- return preScript + recorderScript;
33
+ const clipboardBridgeScript = `
34
+ ;(() => {
35
+ if (window.__bvtRecorderClipboardBridgeInitialized) {
36
+ return;
37
+ }
38
+ window.__bvtRecorderClipboardBridgeInitialized = true;
39
+
40
+ const emitPayload = (payload, attempt = 0) => {
41
+ const reporter = window.__bvt_reportClipboard;
42
+ if (typeof reporter === "function") {
43
+ try {
44
+ reporter(payload);
45
+ } catch (error) {
46
+ console.warn("Clipboard bridge failed to report payload", error);
47
+ }
48
+ return;
49
+ }
50
+ if (attempt < 5) {
51
+ setTimeout(() => emitPayload(payload, attempt + 1), 50 * (attempt + 1));
52
+ }
53
+ };
54
+
55
+ const fileToBase64 = (file) => {
56
+ return new Promise((resolve) => {
57
+ try {
58
+ const reader = new FileReader();
59
+ reader.onload = () => {
60
+ const { result } = reader;
61
+ if (typeof result === "string") {
62
+ const index = result.indexOf("base64,");
63
+ resolve(index !== -1 ? result.substring(index + 7) : result);
64
+ return;
65
+ }
66
+ if (result instanceof ArrayBuffer) {
67
+ const bytes = new Uint8Array(result);
68
+ let binary = "";
69
+ const chunk = 0x8000;
70
+ for (let i = 0; i < bytes.length; i += chunk) {
71
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
72
+ }
73
+ resolve(btoa(binary));
74
+ return;
75
+ }
76
+ resolve(null);
77
+ };
78
+ reader.onerror = () => resolve(null);
79
+ reader.readAsDataURL(file);
80
+ } catch (error) {
81
+ console.warn("Clipboard bridge failed to serialize file", error);
82
+ resolve(null);
83
+ }
84
+ });
85
+ };
86
+
87
+ const handleClipboardEvent = async (event) => {
88
+ try {
89
+ const payload = { trigger: event.type };
90
+ const clipboardData = event.clipboardData;
91
+
92
+ if (clipboardData) {
93
+ try {
94
+ const text = clipboardData.getData("text/plain");
95
+ if (text) {
96
+ payload.text = text;
97
+ }
98
+ } catch (error) {
99
+ console.warn("Clipboard bridge 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
+ }
107
+ } catch (error) {
108
+ console.warn("Clipboard bridge could not read text/html", error);
109
+ }
110
+
111
+ const files = clipboardData.files;
112
+ if (files && files.length > 0) {
113
+ const serialized = [];
114
+ for (const file of files) {
115
+ const data = await fileToBase64(file);
116
+ if (data) {
117
+ serialized.push({
118
+ name: file.name,
119
+ type: file.type,
120
+ lastModified: file.lastModified,
121
+ data,
122
+ });
123
+ }
124
+ }
125
+ if (serialized.length > 0) {
126
+ payload.files = serialized;
127
+ }
128
+ }
129
+ }
130
+
131
+ if (!payload.text) {
132
+ try {
133
+ const selection = window.getSelection?.();
134
+ const selectionText = selection?.toString?.();
135
+ if (selectionText) {
136
+ payload.text = selectionText;
137
+ }
138
+ } catch {
139
+ // Ignore selection access errors.
140
+ }
141
+ }
142
+
143
+ emitPayload(payload);
144
+ } catch (error) {
145
+ console.warn("Clipboard bridge could not process event", error);
146
+ }
147
+ };
148
+
149
+ document.addEventListener(
150
+ "copy",
151
+ (event) => {
152
+ void handleClipboardEvent(event);
153
+ },
154
+ true
155
+ );
156
+ document.addEventListener(
157
+ "cut",
158
+ (event) => {
159
+ void handleClipboardEvent(event);
160
+ },
161
+ true
162
+ );
163
+ })();
164
+ `;
165
+ return preScript + recorderScript + process.env.REMOTE_RECORDER === "true" ? clipboardBridgeScript : `;`;
34
166
  }
35
167
 
36
168
  async function evaluate(frame, script) {
@@ -200,7 +332,7 @@ export class BVTRecorder {
200
332
  this.workspaceService = new PublishService(this.TOKEN);
201
333
  this.pageSet = new Set();
202
334
  this.lastKnownUrlPath = "";
203
- this.world = { attach: () => { } };
335
+ this.world = { attach: () => {} };
204
336
  this.shouldTakeScreenshot = true;
205
337
  this.watcher = null;
206
338
  this.networkEventsFolder = path.join(tmpdir(), "blinq_network_events");
@@ -228,6 +360,8 @@ export class BVTRecorder {
228
360
  updateCommand: "BVTRecorder.updateCommand",
229
361
  browserStateSync: "BrowserService.stateSync",
230
362
  browserStateError: "BrowserService.stateError",
363
+ clipboardPush: "BrowserService.clipboardPush",
364
+ clipboardError: "BrowserService.clipboardError",
231
365
  };
232
366
  bindings = {
233
367
  __bvt_recordCommand: async ({ frame, page, context }, event) => {
@@ -257,6 +391,25 @@ export class BVTRecorder {
257
391
  __bvt_getObject: (_src, obj) => {
258
392
  this.processObject(obj);
259
393
  },
394
+ __bvt_reportClipboard: async ({ page }, payload) => {
395
+ try {
396
+ if (!payload) {
397
+ return;
398
+ }
399
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
400
+ if (activePage && activePage !== page) {
401
+ return;
402
+ }
403
+ const pageUrl = typeof page?.url === "function" ? page.url() : null;
404
+ this.sendEvent(this.events.clipboardPush, {
405
+ data: payload,
406
+ trigger: payload?.trigger ?? "copy",
407
+ pageUrl,
408
+ });
409
+ } catch (error) {
410
+ this.logger.error("Error forwarding clipboard payload from page", error);
411
+ }
412
+ },
260
413
  };
261
414
 
262
415
  getSnapshot = async (attr) => {
@@ -322,7 +475,7 @@ export class BVTRecorder {
322
475
  }
323
476
 
324
477
  // this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
325
- this.world = { attach: () => { } };
478
+ this.world = { attach: () => {} };
326
479
 
327
480
  const ai_config_file = path.join(this.projectDir, "ai_config.json");
328
481
  let ai_config = {};
@@ -824,7 +977,7 @@ export class BVTRecorder {
824
977
  }
825
978
  async closeBrowser() {
826
979
  delete process.env.TEMP_RUN;
827
- await this.watcher.close().then(() => { });
980
+ await this.watcher.close().then(() => {});
828
981
  this.watcher = null;
829
982
  this.previousIndex = null;
830
983
  this.previousHistoryLength = null;
@@ -1381,6 +1534,207 @@ export class BVTRecorder {
1381
1534
  }
1382
1535
  }
1383
1536
 
1537
+ async applyClipboardPayload(message) {
1538
+ const payload = message?.data ?? message;
1539
+ if (!payload) {
1540
+ return;
1541
+ }
1542
+
1543
+ try {
1544
+ if (this.browserEmitter && typeof this.browserEmitter.applyClipboardPayload === "function") {
1545
+ await this.browserEmitter.applyClipboardPayload(payload);
1546
+ return;
1547
+ }
1548
+
1549
+ const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
1550
+ if (!activePage) {
1551
+ this.logger.warn("No active page available to apply clipboard payload");
1552
+ return;
1553
+ }
1554
+
1555
+ await this.injectClipboardIntoPage(activePage, payload);
1556
+ } catch (error) {
1557
+ this.logger.error("Error applying clipboard payload to page", error);
1558
+ this.sendEvent(this.events.clipboardError, {
1559
+ message: "Failed to apply clipboard contents to the remote session",
1560
+ trigger: message?.trigger ?? "paste",
1561
+ });
1562
+ }
1563
+ }
1564
+
1565
+ async injectClipboardIntoPage(page, payload) {
1566
+ if (!page) {
1567
+ return;
1568
+ }
1569
+
1570
+ try {
1571
+ await page
1572
+ .context()
1573
+ .grantPermissions(["clipboard-read", "clipboard-write"])
1574
+ .catch(() => {});
1575
+ await page.evaluate(async (clipboardPayload) => {
1576
+ const toArrayBuffer = (base64) => {
1577
+ if (!base64) {
1578
+ return null;
1579
+ }
1580
+ const binaryString = atob(base64);
1581
+ const len = binaryString.length;
1582
+ const bytes = new Uint8Array(len);
1583
+ for (let i = 0; i < len; i += 1) {
1584
+ bytes[i] = binaryString.charCodeAt(i);
1585
+ }
1586
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
1587
+ };
1588
+
1589
+ const createFileFromPayload = (filePayload) => {
1590
+ const buffer = toArrayBuffer(filePayload?.data);
1591
+ if (!buffer) {
1592
+ return null;
1593
+ }
1594
+ const name = filePayload?.name || "clipboard-file";
1595
+ const type = filePayload?.type || "application/octet-stream";
1596
+ const lastModified = filePayload?.lastModified ?? Date.now();
1597
+ try {
1598
+ return new File([buffer], name, { type, lastModified });
1599
+ } catch (error) {
1600
+ console.warn("Clipboard bridge could not recreate File object", error);
1601
+ return null;
1602
+ }
1603
+ };
1604
+
1605
+ let dataTransfer = null;
1606
+ try {
1607
+ dataTransfer = new DataTransfer();
1608
+ } catch (error) {
1609
+ console.warn("Clipboard bridge could not create DataTransfer", error);
1610
+ }
1611
+
1612
+ if (dataTransfer) {
1613
+ if (clipboardPayload?.text) {
1614
+ try {
1615
+ dataTransfer.setData("text/plain", clipboardPayload.text);
1616
+ } catch (error) {
1617
+ console.warn("Clipboard bridge failed to set text/plain", error);
1618
+ }
1619
+ }
1620
+ if (clipboardPayload?.html) {
1621
+ try {
1622
+ dataTransfer.setData("text/html", clipboardPayload.html);
1623
+ } catch (error) {
1624
+ console.warn("Clipboard bridge failed to set text/html", error);
1625
+ }
1626
+ }
1627
+ if (Array.isArray(clipboardPayload?.files)) {
1628
+ for (const filePayload of clipboardPayload.files) {
1629
+ const file = createFileFromPayload(filePayload);
1630
+ if (file) {
1631
+ try {
1632
+ dataTransfer.items.add(file);
1633
+ } catch (error) {
1634
+ console.warn("Clipboard bridge failed to append file", error);
1635
+ }
1636
+ }
1637
+ }
1638
+ }
1639
+ }
1640
+
1641
+ let target = document.activeElement || document.body;
1642
+ if (!target) {
1643
+ target = document.body || null;
1644
+ }
1645
+
1646
+ let pasteHandled = false;
1647
+ if (dataTransfer && target && typeof target.dispatchEvent === "function") {
1648
+ try {
1649
+ const clipboardEvent = new ClipboardEvent("paste", {
1650
+ clipboardData: dataTransfer,
1651
+ bubbles: true,
1652
+ cancelable: true,
1653
+ });
1654
+ pasteHandled = target.dispatchEvent(clipboardEvent);
1655
+ } catch (error) {
1656
+ console.warn("Clipboard bridge failed to dispatch synthetic paste event", error);
1657
+ }
1658
+ }
1659
+
1660
+ if (pasteHandled) {
1661
+ return;
1662
+ }
1663
+
1664
+ const callLegacyExecCommand = (command, value) => {
1665
+ const execCommand = document && document["execCommand"];
1666
+ if (typeof execCommand === "function") {
1667
+ try {
1668
+ return execCommand.call(document, command, false, value);
1669
+ } catch (error) {
1670
+ console.warn("Clipboard bridge failed to execute legacy command", error);
1671
+ }
1672
+ }
1673
+ return false;
1674
+ };
1675
+
1676
+ if (clipboardPayload?.html) {
1677
+ const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
1678
+ if (inserted) {
1679
+ return;
1680
+ }
1681
+ try {
1682
+ const selection = window.getSelection?.();
1683
+ if (selection && selection.rangeCount > 0) {
1684
+ const range = selection.getRangeAt(0);
1685
+ range.deleteContents();
1686
+ const fragment = range.createContextualFragment(clipboardPayload.html);
1687
+ range.insertNode(fragment);
1688
+ range.collapse(false);
1689
+ return;
1690
+ }
1691
+ } catch (error) {
1692
+ console.warn("Clipboard bridge could not insert HTML via Range APIs", error);
1693
+ }
1694
+ }
1695
+
1696
+ if (clipboardPayload?.text) {
1697
+ const inserted = callLegacyExecCommand("insertText", clipboardPayload.text);
1698
+ if (inserted) {
1699
+ return;
1700
+ }
1701
+ try {
1702
+ const selection = window.getSelection?.();
1703
+ if (selection && selection.rangeCount > 0) {
1704
+ const range = selection.getRangeAt(0);
1705
+ range.deleteContents();
1706
+ range.insertNode(document.createTextNode(clipboardPayload.text));
1707
+ range.collapse(false);
1708
+ return;
1709
+ }
1710
+ } catch (error) {
1711
+ console.warn("Clipboard bridge could not insert text via Range APIs", error);
1712
+ }
1713
+ }
1714
+
1715
+ if (clipboardPayload?.text && target && "value" in target) {
1716
+ try {
1717
+ const input = target;
1718
+ const start = input.selectionStart ?? input.value.length ?? 0;
1719
+ const end = input.selectionEnd ?? input.value.length ?? 0;
1720
+ const value = input.value ?? "";
1721
+ const text = clipboardPayload.text;
1722
+ input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
1723
+ const caret = start + text.length;
1724
+ if (typeof input.setSelectionRange === "function") {
1725
+ input.setSelectionRange(caret, caret);
1726
+ }
1727
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1728
+ } catch (error) {
1729
+ console.warn("Clipboard bridge failed to mutate input element", error);
1730
+ }
1731
+ }
1732
+ }, payload);
1733
+ } catch (error) {
1734
+ throw error;
1735
+ }
1736
+ }
1737
+
1384
1738
  async createTab(url) {
1385
1739
  try {
1386
1740
  await this.browserEmitter?.createTab(url);
@@ -1416,4 +1770,56 @@ export class BVTRecorder {
1416
1770
  });
1417
1771
  }
1418
1772
  }
1773
+
1774
+ async navigateTab({ pageId, url }) {
1775
+ try {
1776
+ if (!pageId || !url) {
1777
+ this.logger.error("navigateTab called without pageId or url", { pageId, url });
1778
+ return;
1779
+ }
1780
+ await this.browserEmitter?.navigateTab(pageId, url);
1781
+ } catch (error) {
1782
+ this.logger.error("Error navigating tab:", error);
1783
+ this.sendEvent(this.events.browserStateError, {
1784
+ message: "Error navigating tab",
1785
+ code: "NAVIGATE_TAB_ERROR",
1786
+ });
1787
+ }
1788
+ }
1789
+
1790
+ async reloadTab(pageId) {
1791
+ try {
1792
+ await this.browserEmitter?.reloadTab(pageId);
1793
+ } catch (error) {
1794
+ this.logger.error("Error reloading tab:", error);
1795
+ this.sendEvent(this.events.browserStateError, {
1796
+ message: "Error reloading tab",
1797
+ code: "RELOAD_TAB_ERROR",
1798
+ });
1799
+ }
1800
+ }
1801
+
1802
+ async goBack(pageId) {
1803
+ try {
1804
+ await this.browserEmitter?.goBack(pageId);
1805
+ } catch (error) {
1806
+ this.logger.error("Error navigating back:", error);
1807
+ this.sendEvent(this.events.browserStateError, {
1808
+ message: "Error navigating back",
1809
+ code: "GO_BACK_ERROR",
1810
+ });
1811
+ }
1812
+ }
1813
+
1814
+ async goForward(pageId) {
1815
+ try {
1816
+ await this.browserEmitter?.goForward(pageId);
1817
+ } catch (error) {
1818
+ this.logger.error("Error navigating forward:", error);
1819
+ this.sendEvent(this.events.browserStateError, {
1820
+ message: "Error navigating forward",
1821
+ code: "GO_FORWARD_ERROR",
1822
+ });
1823
+ }
1824
+ }
1419
1825
  }
@@ -226,6 +226,9 @@ export class RemoteBrowserService extends EventEmitter {
226
226
  this.context = context;
227
227
  this.wsUrlBase = this.CDP_CONNECT_URL.replace(/^http/, "ws") + "/devtools/page/";
228
228
  this.log("🚀 RemoteBrowserService initialized", { CDP_CONNECT_URL });
229
+ this.context.grantPermissions(["clipboard-read", "clipboard-write"]).catch((error) => {
230
+ this.log("⚠️ Failed to pre-grant clipboard permissions", { error });
231
+ });
229
232
  this.initializeListeners();
230
233
  }
231
234
  log(message, data) {
@@ -259,6 +262,21 @@ export class RemoteBrowserService extends EventEmitter {
259
262
  }
260
263
  async initializeListeners() {
261
264
  this.log("📡 Initializing listeners");
265
+ // Initialize with existing pages
266
+ const existingPages = this.context.pages();
267
+ this.log("📄 Found existing pages", { count: existingPages.length });
268
+ for (const page of existingPages) {
269
+ const stableTabId = uuidv4();
270
+ const cdpTargetId = await this.getCdpTargetId(page);
271
+ this.pages.set(stableTabId, { page, cdpTargetId });
272
+ this.log("✅ Existing page added to map", { stableTabId, cdpTargetId, url: page.url() });
273
+ this.attachPageLifecycleListeners(page, stableTabId);
274
+ this.log("🔍 Attached lifecycle listeners to existing page", { stableTabId });
275
+ }
276
+ if (this.pages.size > 0 && !this._selectedPageId) {
277
+ this._selectedPageId = Array.from(this.pages.keys())[0];
278
+ }
279
+ await this.syncState();
262
280
  this.context.on("page", async (page) => {
263
281
  const stableTabId = uuidv4();
264
282
  this.log("🆕 New page event triggered", { stableTabId, url: page.url() });
@@ -276,33 +294,22 @@ export class RemoteBrowserService extends EventEmitter {
276
294
  this._selectedPageId = stableTabId;
277
295
  this.log("🎯 Initial selected page set", { selectedPageId: this._selectedPageId });
278
296
  }
297
+ this.attachPageLifecycleListeners(page, stableTabId);
298
+ await this.syncState();
299
+ });
300
+ }
301
+ attachPageLifecycleListeners(page, stableTabId) {
302
+ page.on("load", () => this.syncState());
303
+ page.on("framenavigated", () => this.syncState());
304
+ page.on("close", async () => {
305
+ this.log("🗑️ Page close event", { stableTabId });
306
+ this.pages.delete(stableTabId);
307
+ if (this._selectedPageId === stableTabId) {
308
+ this._selectedPageId = this.pages.size > 0 ? Array.from(this.pages.keys())[0] : null;
309
+ this.log("🔄 Selected page changed after close", { newSelectedId: this._selectedPageId });
310
+ }
279
311
  await this.syncState();
280
- // Add listeners
281
- page.on("load", () => this.syncState());
282
- page.on("framenavigated", () => this.syncState());
283
- page.on("close", async () => {
284
- this.log("🗑️ Page close event", { stableTabId });
285
- this.pages.delete(stableTabId);
286
- if (this._selectedPageId === stableTabId) {
287
- this._selectedPageId = this.pages.size > 0 ? Array.from(this.pages.keys())[0] : null;
288
- this.log("🔄 Selected page changed after close", { newSelectedId: this._selectedPageId });
289
- }
290
- await this.syncState();
291
- });
292
312
  });
293
- // Initialize with existing pages
294
- const existingPages = this.context.pages();
295
- this.log("📄 Found existing pages", { count: existingPages.length });
296
- for (const page of existingPages) {
297
- const stableTabId = uuidv4();
298
- const cdpTargetId = await this.getCdpTargetId(page);
299
- this.pages.set(stableTabId, { page, cdpTargetId });
300
- this.log("✅ Existing page added to map", { stableTabId, cdpTargetId, url: page.url() });
301
- }
302
- if (this.pages.size > 0 && !this._selectedPageId) {
303
- this._selectedPageId = Array.from(this.pages.keys())[0];
304
- }
305
- await this.syncState();
306
313
  }
307
314
  async syncState() {
308
315
  try {
@@ -337,12 +344,14 @@ export class RemoteBrowserService extends EventEmitter {
337
344
  if (!wsDebuggerUrl) {
338
345
  this.log("⚠️ Could not get CDP ID, wsDebuggerUrl will be empty", { stableTabId });
339
346
  }
347
+ const title = await pageInfo.page.title();
340
348
  pagesData.push({
341
349
  id: stableTabId,
342
- title: await pageInfo.page.title(),
350
+ title,
343
351
  url: pageInfo.page.url(),
344
352
  wsDebuggerUrl: wsDebuggerUrl, // Use the constructed URL
345
353
  });
354
+ this.log(`🟥Page data: ${JSON.stringify(pagesData, null, 2)}`);
346
355
  }
347
356
  catch (error) {
348
357
  this.log("❌ Error getting page data", { stableTabId, error });
@@ -418,6 +427,260 @@ export class RemoteBrowserService extends EventEmitter {
418
427
  this.log("❌ Error selecting tab", { error });
419
428
  }
420
429
  }
430
+ getPageInfo(stableTabId) {
431
+ if (!stableTabId) {
432
+ this.log("⚠️ Operation requested without a selected tab");
433
+ return null;
434
+ }
435
+ const pageInfo = this.pages.get(stableTabId);
436
+ if (!pageInfo) {
437
+ this.log("⚠️ Page not found for operation", { stableTabId });
438
+ return null;
439
+ }
440
+ return pageInfo;
441
+ }
442
+ async navigateTab(stableTabId, url) {
443
+ const pageInfo = this.getPageInfo(stableTabId);
444
+ if (!pageInfo) {
445
+ return;
446
+ }
447
+ try {
448
+ this.log("🌐 Navigating tab", { stableTabId, url });
449
+ await pageInfo.page.goto(url, { waitUntil: "domcontentloaded" });
450
+ this._selectedPageId = stableTabId;
451
+ await this.syncState();
452
+ }
453
+ catch (error) {
454
+ this.log("❌ Error navigating tab", { stableTabId, url, error });
455
+ throw error;
456
+ }
457
+ }
458
+ async reloadTab(stableTabId) {
459
+ const pageInfo = this.getPageInfo(stableTabId);
460
+ if (!pageInfo) {
461
+ return;
462
+ }
463
+ try {
464
+ this.log("🔄 Reloading tab", { stableTabId, url: pageInfo.page.url() });
465
+ await pageInfo.page.reload({ waitUntil: "domcontentloaded" });
466
+ await this.syncState();
467
+ }
468
+ catch (error) {
469
+ this.log("❌ Error reloading tab", { stableTabId, error });
470
+ throw error;
471
+ }
472
+ }
473
+ async goBack(stableTabId) {
474
+ const pageInfo = this.getPageInfo(stableTabId);
475
+ if (!pageInfo) {
476
+ return;
477
+ }
478
+ try {
479
+ this.log("⬅️ Navigating back", { stableTabId });
480
+ const response = await pageInfo.page.goBack({ waitUntil: "domcontentloaded" });
481
+ if (!response) {
482
+ this.log("ℹ️ No history entry to go back to", { stableTabId });
483
+ }
484
+ await this.syncState();
485
+ }
486
+ catch (error) {
487
+ this.log("❌ Error navigating back", { stableTabId, error });
488
+ throw error;
489
+ }
490
+ }
491
+ async goForward(stableTabId) {
492
+ const pageInfo = this.getPageInfo(stableTabId);
493
+ if (!pageInfo) {
494
+ return;
495
+ }
496
+ try {
497
+ this.log("➡️ Navigating forward", { stableTabId });
498
+ const response = await pageInfo.page.goForward({ waitUntil: "domcontentloaded" });
499
+ if (!response) {
500
+ this.log("ℹ️ No history entry to go forward to", { stableTabId });
501
+ }
502
+ await this.syncState();
503
+ }
504
+ catch (error) {
505
+ this.log("❌ Error navigating forward", { stableTabId, error });
506
+ throw error;
507
+ }
508
+ }
509
+ async applyClipboardPayload(payload) {
510
+ const pageInfo = this.getPageInfo(this._selectedPageId);
511
+ if (!pageInfo) {
512
+ this.log("⚠️ No active page available for clipboard payload");
513
+ return;
514
+ }
515
+ try {
516
+ await pageInfo.page
517
+ .context()
518
+ .grantPermissions(["clipboard-read", "clipboard-write"])
519
+ .catch(() => { });
520
+ await pageInfo.page.evaluate(async (clipboardPayload) => {
521
+ const toArrayBuffer = (base64) => {
522
+ if (!base64) {
523
+ return null;
524
+ }
525
+ const binary = atob(base64);
526
+ const length = binary.length;
527
+ const bytes = new Uint8Array(length);
528
+ for (let index = 0; index < length; index += 1) {
529
+ bytes[index] = binary.charCodeAt(index);
530
+ }
531
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
532
+ };
533
+ const createFileFromPayload = (filePayload) => {
534
+ const buffer = toArrayBuffer(filePayload?.data);
535
+ if (!buffer) {
536
+ return null;
537
+ }
538
+ const name = filePayload?.name || "clipboard-file";
539
+ const type = filePayload?.type || "application/octet-stream";
540
+ const lastModified = filePayload?.lastModified ?? Date.now();
541
+ try {
542
+ return new File([buffer], name, { type, lastModified });
543
+ }
544
+ catch (error) {
545
+ console.warn("Clipboard bridge could not recreate File", error);
546
+ return null;
547
+ }
548
+ };
549
+ let dataTransfer = null;
550
+ try {
551
+ dataTransfer = new DataTransfer();
552
+ }
553
+ catch (error) {
554
+ console.warn("Clipboard bridge could not create DataTransfer", error);
555
+ }
556
+ const callLegacyExecCommand = (command, value) => {
557
+ const execCommand = document.execCommand;
558
+ if (typeof execCommand === "function") {
559
+ try {
560
+ return execCommand.call(document, command, false, value);
561
+ }
562
+ catch (error) {
563
+ console.warn("Clipboard bridge failed to execute legacy command", error);
564
+ }
565
+ }
566
+ return false;
567
+ };
568
+ if (dataTransfer) {
569
+ if (clipboardPayload?.text) {
570
+ try {
571
+ dataTransfer.setData("text/plain", clipboardPayload.text);
572
+ }
573
+ catch (error) {
574
+ console.warn("Clipboard bridge failed to attach text/plain", error);
575
+ }
576
+ }
577
+ if (clipboardPayload?.html) {
578
+ try {
579
+ dataTransfer.setData("text/html", clipboardPayload.html);
580
+ }
581
+ catch (error) {
582
+ console.warn("Clipboard bridge failed to attach text/html", error);
583
+ }
584
+ }
585
+ if (Array.isArray(clipboardPayload?.files)) {
586
+ for (const filePayload of clipboardPayload.files) {
587
+ const file = createFileFromPayload(filePayload);
588
+ if (file) {
589
+ try {
590
+ dataTransfer.items.add(file);
591
+ }
592
+ catch (error) {
593
+ console.warn("Clipboard bridge failed to attach file", error);
594
+ }
595
+ }
596
+ }
597
+ }
598
+ }
599
+ let target = document.activeElement;
600
+ if (!target || target === document.body) {
601
+ target = document.body;
602
+ }
603
+ let pasteHandled = false;
604
+ if (dataTransfer && target && typeof target.dispatchEvent === "function") {
605
+ try {
606
+ const clipboardEvent = new ClipboardEvent("paste", {
607
+ clipboardData: dataTransfer,
608
+ bubbles: true,
609
+ cancelable: true,
610
+ });
611
+ pasteHandled = target.dispatchEvent(clipboardEvent);
612
+ }
613
+ catch (error) {
614
+ console.warn("Clipboard bridge failed to dispatch paste event", error);
615
+ }
616
+ }
617
+ if (pasteHandled) {
618
+ return;
619
+ }
620
+ if (clipboardPayload?.html) {
621
+ const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
622
+ if (inserted) {
623
+ return;
624
+ }
625
+ try {
626
+ const selection = window.getSelection?.();
627
+ if (selection && selection.rangeCount > 0) {
628
+ const range = selection.getRangeAt(0);
629
+ range.deleteContents();
630
+ const fragment = range.createContextualFragment(clipboardPayload.html);
631
+ range.insertNode(fragment);
632
+ range.collapse(false);
633
+ return;
634
+ }
635
+ }
636
+ catch (error) {
637
+ console.warn("Clipboard bridge could not insert HTML via Range APIs", error);
638
+ }
639
+ }
640
+ if (clipboardPayload?.text) {
641
+ const inserted = callLegacyExecCommand("insertText", clipboardPayload.text);
642
+ if (inserted) {
643
+ return;
644
+ }
645
+ try {
646
+ const selection = window.getSelection?.();
647
+ if (selection && selection.rangeCount > 0) {
648
+ const range = selection.getRangeAt(0);
649
+ range.deleteContents();
650
+ range.insertNode(document.createTextNode(clipboardPayload.text));
651
+ range.collapse(false);
652
+ return;
653
+ }
654
+ }
655
+ catch (error) {
656
+ console.warn("Clipboard bridge could not insert text via Range APIs", error);
657
+ }
658
+ }
659
+ if (clipboardPayload?.text && target && "value" in target) {
660
+ try {
661
+ const input = target;
662
+ const start = input.selectionStart ?? input.value.length ?? 0;
663
+ const end = input.selectionEnd ?? input.value.length ?? 0;
664
+ const value = input.value ?? "";
665
+ const text = clipboardPayload.text;
666
+ input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
667
+ const caret = start + text.length;
668
+ if (typeof input.setSelectionRange === "function") {
669
+ input.setSelectionRange(caret, caret);
670
+ }
671
+ input.dispatchEvent(new Event("input", { bubbles: true }));
672
+ }
673
+ catch (error) {
674
+ console.warn("Clipboard bridge failed to insert text into input", error);
675
+ }
676
+ }
677
+ }, payload);
678
+ }
679
+ catch (error) {
680
+ this.log("❌ Error applying clipboard payload", { error });
681
+ throw error;
682
+ }
683
+ }
421
684
  getSelectedPage() {
422
685
  const pageInfo = this._selectedPageId ? this.pages.get(this._selectedPageId) : null;
423
686
  this.log("🔍 Getting selected page", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev-blinq/cucumber_client",
3
- "version": "1.0.1612-dev",
3
+ "version": "1.0.1614-dev",
4
4
  "description": " ",
5
5
  "main": "bin/index.js",
6
6
  "types": "bin/index.d.ts",