@dev-blinq/cucumber_client 1.0.1613-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
|
-
|
|
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) {
|
|
@@ -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) => {
|
|
@@ -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
|
|
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", {
|