@dev-blinq/cucumber_client 1.0.1624-dev â 1.0.1625-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.
|
@@ -327,10 +327,14 @@ async function BVTRecorderInit({ envName, projectDir, roomId, TOKEN, socket = nu
|
|
|
327
327
|
console.log("Received browserUI.goForward", input);
|
|
328
328
|
await recorder.goForward(input);
|
|
329
329
|
},
|
|
330
|
-
"browserUI.
|
|
331
|
-
console.log("Received browserUI.
|
|
330
|
+
"browserUI.writeToClipboard": async (input) => {
|
|
331
|
+
console.log("Received browserUI.writeToClipboard");
|
|
332
332
|
await recorder.applyClipboardPayload(input);
|
|
333
333
|
},
|
|
334
|
+
"browserUI.readToClipboard": async (input) => {
|
|
335
|
+
console.log("Received browserUI.readToClipboard");
|
|
336
|
+
await recorder.readClipboardPayload(input);
|
|
337
|
+
},
|
|
334
338
|
"recorderWindow.cleanup": async (input) => {
|
|
335
339
|
return recorder.cleanup(input);
|
|
336
340
|
},
|
|
@@ -7,8 +7,7 @@ import { getImplementedSteps, parseRouteFiles } from "./implemented_steps.js";
|
|
|
7
7
|
import { NamesService, RemoteBrowserService, PublishService } from "./services.js";
|
|
8
8
|
import { BVTStepRunner } from "./step_runner.js";
|
|
9
9
|
import { readFile, writeFile } from "fs/promises";
|
|
10
|
-
import {
|
|
11
|
-
import { updateFeatureFile } from "./update_feature.js";
|
|
10
|
+
import { loadStepDefinitions, getCommandsForImplementedStep } from "./step_utils.js";
|
|
12
11
|
import { parseStepTextParameters } from "../cucumber/utils.js";
|
|
13
12
|
import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
|
|
14
13
|
import chokidar from "chokidar";
|
|
@@ -18,37 +17,36 @@ import socketLogger from "../utils/socket_logger.js";
|
|
|
18
17
|
import { tmpdir } from "os";
|
|
19
18
|
import { faker } from "@faker-js/faker/locale/en_US";
|
|
20
19
|
import { chromium } from "playwright-core";
|
|
20
|
+
|
|
21
21
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
22
22
|
|
|
23
23
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
window.__bvt_Recorder_config = ${JSON.stringify(config ?? null)};
|
|
27
|
-
window.__PW_options = ${JSON.stringify(options ?? null)};
|
|
28
|
-
`;
|
|
29
|
-
const recorderScript = readFileSync(
|
|
30
|
-
path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"),
|
|
31
|
-
"utf8"
|
|
32
|
-
);
|
|
33
|
-
const clipboardBridgeScript = `
|
|
24
|
+
|
|
25
|
+
const clipboardBridgeScript = `
|
|
34
26
|
;(() => {
|
|
35
27
|
if (window.__bvtRecorderClipboardBridgeInitialized) {
|
|
28
|
+
console.log('[ClipboardBridge] Already initialized, skipping');
|
|
36
29
|
return;
|
|
37
30
|
}
|
|
38
31
|
window.__bvtRecorderClipboardBridgeInitialized = true;
|
|
32
|
+
console.log('[ClipboardBridge] Initializing clipboard bridge');
|
|
39
33
|
|
|
40
34
|
const emitPayload = (payload, attempt = 0) => {
|
|
41
35
|
const reporter = window.__bvt_reportClipboard;
|
|
42
36
|
if (typeof reporter === "function") {
|
|
43
37
|
try {
|
|
38
|
+
console.log('[ClipboardBridge] Reporting clipboard payload:', payload);
|
|
44
39
|
reporter(payload);
|
|
45
40
|
} catch (error) {
|
|
46
|
-
console.warn("
|
|
41
|
+
console.warn("[ClipboardBridge] Failed to report payload", error);
|
|
47
42
|
}
|
|
48
43
|
return;
|
|
49
44
|
}
|
|
50
45
|
if (attempt < 5) {
|
|
46
|
+
console.log('[ClipboardBridge] Reporter not ready, retrying...', attempt);
|
|
51
47
|
setTimeout(() => emitPayload(payload, attempt + 1), 50 * (attempt + 1));
|
|
48
|
+
} else {
|
|
49
|
+
console.warn('[ClipboardBridge] Reporter never became available');
|
|
52
50
|
}
|
|
53
51
|
};
|
|
54
52
|
|
|
@@ -78,7 +76,7 @@ export function getInitScript(config, options) {
|
|
|
78
76
|
reader.onerror = () => resolve(null);
|
|
79
77
|
reader.readAsDataURL(file);
|
|
80
78
|
} catch (error) {
|
|
81
|
-
console.warn("
|
|
79
|
+
console.warn("[ClipboardBridge] Failed to serialize file", error);
|
|
82
80
|
resolve(null);
|
|
83
81
|
}
|
|
84
82
|
});
|
|
@@ -86,6 +84,7 @@ export function getInitScript(config, options) {
|
|
|
86
84
|
|
|
87
85
|
const handleClipboardEvent = async (event) => {
|
|
88
86
|
try {
|
|
87
|
+
console.log('[ClipboardBridge] Handling clipboard event:', event.type);
|
|
89
88
|
const payload = { trigger: event.type };
|
|
90
89
|
const clipboardData = event.clipboardData;
|
|
91
90
|
|
|
@@ -94,22 +93,25 @@ export function getInitScript(config, options) {
|
|
|
94
93
|
const text = clipboardData.getData("text/plain");
|
|
95
94
|
if (text) {
|
|
96
95
|
payload.text = text;
|
|
96
|
+
console.log('[ClipboardBridge] Captured text:', text.substring(0, 50));
|
|
97
97
|
}
|
|
98
98
|
} catch (error) {
|
|
99
|
-
console.warn("
|
|
99
|
+
console.warn("[ClipboardBridge] Could not read text/plain", error);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
try {
|
|
103
103
|
const html = clipboardData.getData("text/html");
|
|
104
104
|
if (html) {
|
|
105
105
|
payload.html = html;
|
|
106
|
+
console.log('[ClipboardBridge] Captured HTML:', html.substring(0, 50));
|
|
106
107
|
}
|
|
107
108
|
} catch (error) {
|
|
108
|
-
console.warn("
|
|
109
|
+
console.warn("[ClipboardBridge] Could not read text/html", error);
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
const files = clipboardData.files;
|
|
112
113
|
if (files && files.length > 0) {
|
|
114
|
+
console.log('[ClipboardBridge] Processing files:', files.length);
|
|
113
115
|
const serialized = [];
|
|
114
116
|
for (const file of files) {
|
|
115
117
|
const data = await fileToBase64(file);
|
|
@@ -134,6 +136,7 @@ export function getInitScript(config, options) {
|
|
|
134
136
|
const selectionText = selection?.toString?.();
|
|
135
137
|
if (selectionText) {
|
|
136
138
|
payload.text = selectionText;
|
|
139
|
+
console.log('[ClipboardBridge] Using selection text:', selectionText.substring(0, 50));
|
|
137
140
|
}
|
|
138
141
|
} catch {
|
|
139
142
|
// Ignore selection access errors.
|
|
@@ -142,10 +145,170 @@ export function getInitScript(config, options) {
|
|
|
142
145
|
|
|
143
146
|
emitPayload(payload);
|
|
144
147
|
} catch (error) {
|
|
145
|
-
console.warn("
|
|
148
|
+
console.warn("[ClipboardBridge] Could not process event", error);
|
|
146
149
|
}
|
|
147
150
|
};
|
|
148
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
|
|
149
312
|
document.addEventListener(
|
|
150
313
|
"copy",
|
|
151
314
|
(event) => {
|
|
@@ -160,8 +323,20 @@ export function getInitScript(config, options) {
|
|
|
160
323
|
},
|
|
161
324
|
true
|
|
162
325
|
);
|
|
326
|
+
|
|
327
|
+
console.log('[ClipboardBridge] Clipboard bridge initialized successfully');
|
|
163
328
|
})();
|
|
329
|
+
`;
|
|
330
|
+
|
|
331
|
+
export function getInitScript(config, options) {
|
|
332
|
+
const preScript = `
|
|
333
|
+
window.__bvt_Recorder_config = ${JSON.stringify(config ?? null)};
|
|
334
|
+
window.__PW_options = ${JSON.stringify(options ?? null)};
|
|
164
335
|
`;
|
|
336
|
+
const recorderScript = readFileSync(
|
|
337
|
+
path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"),
|
|
338
|
+
"utf8"
|
|
339
|
+
);
|
|
165
340
|
return preScript + recorderScript + clipboardBridgeScript;
|
|
166
341
|
}
|
|
167
342
|
|
|
@@ -1539,25 +1714,55 @@ export class BVTRecorder {
|
|
|
1539
1714
|
|
|
1540
1715
|
async applyClipboardPayload(message) {
|
|
1541
1716
|
const payload = message?.data ?? message;
|
|
1717
|
+
|
|
1718
|
+
this.logger.info("[BVTRecorder] applyClipboardPayload called", {
|
|
1719
|
+
hasPayload: !!payload,
|
|
1720
|
+
hasText: !!payload?.text,
|
|
1721
|
+
hasHtml: !!payload?.html,
|
|
1722
|
+
trigger: message?.trigger,
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1542
1725
|
if (!payload) {
|
|
1726
|
+
this.logger.warn("[BVTRecorder] No payload provided");
|
|
1543
1727
|
return;
|
|
1544
1728
|
}
|
|
1545
1729
|
|
|
1546
1730
|
try {
|
|
1547
1731
|
if (this.browserEmitter && typeof this.browserEmitter.applyClipboardPayload === "function") {
|
|
1732
|
+
this.logger.info("[BVTRecorder] Using RemoteBrowserService to apply clipboard");
|
|
1548
1733
|
await this.browserEmitter.applyClipboardPayload(payload);
|
|
1549
1734
|
return;
|
|
1550
1735
|
}
|
|
1551
1736
|
|
|
1552
1737
|
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
1553
1738
|
if (!activePage) {
|
|
1554
|
-
this.logger.warn("No active page available
|
|
1739
|
+
this.logger.warn("[BVTRecorder] No active page available");
|
|
1555
1740
|
return;
|
|
1556
1741
|
}
|
|
1557
1742
|
|
|
1558
|
-
|
|
1743
|
+
this.logger.info("[BVTRecorder] Applying clipboard to page", {
|
|
1744
|
+
url: activePage.url(),
|
|
1745
|
+
isClosed: activePage.isClosed(),
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
const result = await activePage.evaluate((clipboardData) => {
|
|
1749
|
+
console.log("[Page] Executing clipboard application", clipboardData);
|
|
1750
|
+
if (typeof window.__bvt_applyClipboardData === "function") {
|
|
1751
|
+
return window.__bvt_applyClipboardData(clipboardData);
|
|
1752
|
+
}
|
|
1753
|
+
console.error("[Page] __bvt_applyClipboardData function not found!");
|
|
1754
|
+
return false;
|
|
1755
|
+
}, payload);
|
|
1756
|
+
|
|
1757
|
+
this.logger.info("[BVTRecorder] Clipboard application result:", result);
|
|
1758
|
+
|
|
1759
|
+
if (!result) {
|
|
1760
|
+
this.logger.warn("[BVTRecorder] Clipboard data not applied successfully");
|
|
1761
|
+
} else {
|
|
1762
|
+
this.logger.info("[BVTRecorder] Clipboard data applied successfully");
|
|
1763
|
+
}
|
|
1559
1764
|
} catch (error) {
|
|
1560
|
-
this.logger.error("Error applying clipboard payload
|
|
1765
|
+
this.logger.error("[BVTRecorder] Error applying clipboard payload", error);
|
|
1561
1766
|
this.sendEvent(this.events.clipboardError, {
|
|
1562
1767
|
message: "Failed to apply clipboard contents to the remote session",
|
|
1563
1768
|
trigger: message?.trigger ?? "paste",
|
|
@@ -1565,6 +1770,152 @@ export class BVTRecorder {
|
|
|
1565
1770
|
}
|
|
1566
1771
|
}
|
|
1567
1772
|
|
|
1773
|
+
hasClipboardPayload(payload) {
|
|
1774
|
+
return Boolean(
|
|
1775
|
+
payload && (payload.text || payload.html || (Array.isArray(payload.files) && payload.files.length > 0))
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
async collectClipboardFromPage(page) {
|
|
1780
|
+
if (!page) {
|
|
1781
|
+
this.logger.warn("[BVTRecorder] No page available to collect clipboard data");
|
|
1782
|
+
return null;
|
|
1783
|
+
}
|
|
1784
|
+
try {
|
|
1785
|
+
await page
|
|
1786
|
+
.context()
|
|
1787
|
+
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
1788
|
+
.catch((error) => {
|
|
1789
|
+
this.logger.warn("[BVTRecorder] Failed to grant clipboard permissions before read", error);
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
const payload = await page.evaluate(async () => {
|
|
1793
|
+
const result = {};
|
|
1794
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
|
1795
|
+
return result;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
const arrayBufferToBase64 = (buffer) => {
|
|
1799
|
+
let binary = "";
|
|
1800
|
+
const bytes = new Uint8Array(buffer);
|
|
1801
|
+
const chunkSize = 0x8000;
|
|
1802
|
+
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
1803
|
+
const chunk = bytes.subarray(index, index + chunkSize);
|
|
1804
|
+
binary += String.fromCharCode(...chunk);
|
|
1805
|
+
}
|
|
1806
|
+
return btoa(binary);
|
|
1807
|
+
};
|
|
1808
|
+
|
|
1809
|
+
const files = [];
|
|
1810
|
+
|
|
1811
|
+
if (typeof navigator.clipboard.read === "function") {
|
|
1812
|
+
try {
|
|
1813
|
+
const items = await navigator.clipboard.read();
|
|
1814
|
+
for (const item of items) {
|
|
1815
|
+
if (item.types.includes("text/html") && !result.html) {
|
|
1816
|
+
const blob = await item.getType("text/html");
|
|
1817
|
+
result.html = await blob.text();
|
|
1818
|
+
}
|
|
1819
|
+
if (item.types.includes("text/plain") && !result.text) {
|
|
1820
|
+
const blob = await item.getType("text/plain");
|
|
1821
|
+
result.text = await blob.text();
|
|
1822
|
+
}
|
|
1823
|
+
for (const type of item.types) {
|
|
1824
|
+
if (type.startsWith("text/")) {
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
try {
|
|
1828
|
+
const blob = await item.getType(type);
|
|
1829
|
+
const buffer = await blob.arrayBuffer();
|
|
1830
|
+
files.push({
|
|
1831
|
+
name: `clipboard-file-${files.length + 1}`,
|
|
1832
|
+
type,
|
|
1833
|
+
lastModified: Date.now(),
|
|
1834
|
+
data: arrayBufferToBase64(buffer),
|
|
1835
|
+
});
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
console.warn("[BVTRecorder] Failed to serialize clipboard blob", { type, error });
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
} catch (error) {
|
|
1842
|
+
console.warn("[BVTRecorder] navigator.clipboard.read failed", error);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
if (!result.text && typeof navigator.clipboard.readText === "function") {
|
|
1847
|
+
try {
|
|
1848
|
+
const text = await navigator.clipboard.readText();
|
|
1849
|
+
if (text) {
|
|
1850
|
+
result.text = text;
|
|
1851
|
+
}
|
|
1852
|
+
} catch (error) {
|
|
1853
|
+
console.warn("[BVTRecorder] navigator.clipboard.readText failed", error);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
if (!result.text) {
|
|
1858
|
+
const selection = window.getSelection?.()?.toString?.();
|
|
1859
|
+
if (selection) {
|
|
1860
|
+
result.text = selection;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
if (files.length > 0) {
|
|
1865
|
+
result.files = files;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
return result;
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
return payload;
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
this.logger.error("[BVTRecorder] Error collecting clipboard payload", error);
|
|
1874
|
+
return null;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
async readClipboardPayload(message) {
|
|
1879
|
+
try {
|
|
1880
|
+
let payload = null;
|
|
1881
|
+
if (this.browserEmitter && typeof this.browserEmitter.readClipboardPayload === "function") {
|
|
1882
|
+
payload = await this.browserEmitter.readClipboardPayload();
|
|
1883
|
+
} else {
|
|
1884
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
1885
|
+
payload = await this.collectClipboardFromPage(activePage);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
if (this.hasClipboardPayload(payload)) {
|
|
1889
|
+
this.logger.info("[BVTRecorder] Remote clipboard payload ready", {
|
|
1890
|
+
hasText: !!payload.text,
|
|
1891
|
+
hasHtml: !!payload.html,
|
|
1892
|
+
files: payload.files?.length ?? 0,
|
|
1893
|
+
});
|
|
1894
|
+
this.sendEvent(this.events.clipboardPush, {
|
|
1895
|
+
data: payload,
|
|
1896
|
+
trigger: message?.trigger ?? "copy",
|
|
1897
|
+
origin: message?.source ?? "browserUI",
|
|
1898
|
+
});
|
|
1899
|
+
return payload;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
this.logger.warn("[BVTRecorder] Remote clipboard payload empty or unavailable");
|
|
1903
|
+
this.sendEvent(this.events.clipboardError, {
|
|
1904
|
+
message: "Remote clipboard is empty",
|
|
1905
|
+
trigger: message?.trigger ?? "copy",
|
|
1906
|
+
});
|
|
1907
|
+
return null;
|
|
1908
|
+
} catch (error) {
|
|
1909
|
+
this.logger.error("[BVTRecorder] Error reading clipboard payload", error);
|
|
1910
|
+
this.sendEvent(this.events.clipboardError, {
|
|
1911
|
+
message: "Failed to read clipboard contents from the remote session",
|
|
1912
|
+
trigger: message?.trigger ?? "copy",
|
|
1913
|
+
details: error instanceof Error ? error.message : String(error),
|
|
1914
|
+
});
|
|
1915
|
+
throw error;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1568
1919
|
async injectClipboardIntoPage(page, payload) {
|
|
1569
1920
|
if (!page) {
|
|
1570
1921
|
return;
|
|
@@ -1576,6 +1927,7 @@ export class BVTRecorder {
|
|
|
1576
1927
|
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
1577
1928
|
.catch(() => {});
|
|
1578
1929
|
await page.evaluate(async (clipboardPayload) => {
|
|
1930
|
+
console.log("Injecting clipboard payload", clipboardPayload);
|
|
1579
1931
|
const toArrayBuffer = (base64) => {
|
|
1580
1932
|
if (!base64) {
|
|
1581
1933
|
return null;
|
|
@@ -220,6 +220,15 @@ export class RemoteBrowserService extends EventEmitter {
|
|
|
220
220
|
pages = new Map();
|
|
221
221
|
_selectedPageId = null;
|
|
222
222
|
wsUrlBase; // Store the base URL
|
|
223
|
+
hasClipboardData(payload) {
|
|
224
|
+
return Boolean(payload && (payload.text || payload.html || (Array.isArray(payload.files) && payload.files.length > 0)));
|
|
225
|
+
}
|
|
226
|
+
getSelectedPageInfo() {
|
|
227
|
+
if (!this._selectedPageId) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
return this.pages.get(this._selectedPageId) ?? null;
|
|
231
|
+
}
|
|
223
232
|
constructor({ CDP_CONNECT_URL, context }) {
|
|
224
233
|
super();
|
|
225
234
|
this.CDP_CONNECT_URL = CDP_CONNECT_URL;
|
|
@@ -507,182 +516,247 @@ export class RemoteBrowserService extends EventEmitter {
|
|
|
507
516
|
}
|
|
508
517
|
}
|
|
509
518
|
async applyClipboardPayload(payload) {
|
|
519
|
+
this.log("đ applyClipboardPayload called", {
|
|
520
|
+
hasText: !!payload?.text,
|
|
521
|
+
hasHtml: !!payload?.html,
|
|
522
|
+
hasFiles: !!payload?.files,
|
|
523
|
+
textLength: payload?.text?.length,
|
|
524
|
+
htmlLength: payload?.html?.length,
|
|
525
|
+
filesCount: payload?.files?.length,
|
|
526
|
+
});
|
|
510
527
|
const pageInfo = this.getPageInfo(this._selectedPageId);
|
|
511
528
|
if (!pageInfo) {
|
|
512
|
-
this.log("â ī¸ No active page available for clipboard payload"
|
|
529
|
+
this.log("â ī¸ No active page available for clipboard payload", {
|
|
530
|
+
selectedPageId: this._selectedPageId,
|
|
531
|
+
totalPages: this.pages.size,
|
|
532
|
+
});
|
|
513
533
|
return;
|
|
514
534
|
}
|
|
535
|
+
this.log("đ Target page info", {
|
|
536
|
+
stableTabId: this._selectedPageId,
|
|
537
|
+
url: pageInfo.page.url(),
|
|
538
|
+
isClosed: pageInfo.page.isClosed(),
|
|
539
|
+
title: await pageInfo.page.title().catch(() => "unknown"),
|
|
540
|
+
});
|
|
515
541
|
try {
|
|
542
|
+
// Grant permissions first
|
|
543
|
+
this.log("đ Attempting to grant clipboard permissions");
|
|
516
544
|
await pageInfo.page
|
|
517
545
|
.context()
|
|
518
546
|
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
519
|
-
.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
};
|
|
549
|
-
let dataTransfer = null;
|
|
547
|
+
.then(() => {
|
|
548
|
+
this.log("â
Clipboard permissions granted");
|
|
549
|
+
})
|
|
550
|
+
.catch((error) => {
|
|
551
|
+
this.log("â ī¸ Failed to grant clipboard permissions", { error: error.message });
|
|
552
|
+
});
|
|
553
|
+
// Apply the clipboard data using the injected function
|
|
554
|
+
this.log("đ Executing clipboard application script", {
|
|
555
|
+
payloadPreview: {
|
|
556
|
+
text: payload?.text?.substring(0, 100),
|
|
557
|
+
html: payload?.html?.substring(0, 100),
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
const result = await pageInfo.page.evaluate((clipboardData) => {
|
|
561
|
+
console.log("[RemoteBrowser->Page] Starting clipboard application");
|
|
562
|
+
console.log("[RemoteBrowser->Page] Payload:", {
|
|
563
|
+
hasText: !!clipboardData?.text,
|
|
564
|
+
hasHtml: !!clipboardData?.html,
|
|
565
|
+
text: clipboardData?.text?.substring(0, 50),
|
|
566
|
+
html: clipboardData?.html?.substring(0, 50),
|
|
567
|
+
});
|
|
568
|
+
// Check if the function exists
|
|
569
|
+
if (typeof window.__bvt_applyClipboardData !== "function") {
|
|
570
|
+
console.error("[RemoteBrowser->Page] â __bvt_applyClipboardData function not found!");
|
|
571
|
+
console.log("[RemoteBrowser->Page] Available window properties:", Object.keys(window).filter((k) => k.includes("bvt")));
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
console.log("[RemoteBrowser->Page] â
__bvt_applyClipboardData function found, calling it");
|
|
550
575
|
try {
|
|
551
|
-
|
|
576
|
+
const result = window.__bvt_applyClipboardData(clipboardData);
|
|
577
|
+
console.log("[RemoteBrowser->Page] Function returned:", result);
|
|
578
|
+
return result;
|
|
552
579
|
}
|
|
553
580
|
catch (error) {
|
|
554
|
-
console.
|
|
581
|
+
console.error("[RemoteBrowser->Page] â Error calling __bvt_applyClipboardData:", error);
|
|
582
|
+
return false;
|
|
555
583
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
584
|
+
}, payload);
|
|
585
|
+
this.log("đ Clipboard application result", {
|
|
586
|
+
success: result,
|
|
587
|
+
selectedPageId: this._selectedPageId,
|
|
588
|
+
});
|
|
589
|
+
if (!result) {
|
|
590
|
+
this.log("â ī¸ Clipboard script returned false - data may not have been applied");
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
this.log("â
Clipboard payload applied successfully");
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
this.log("â Error applying clipboard payload", {
|
|
598
|
+
error: error instanceof Error ? error.message : String(error),
|
|
599
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
600
|
+
selectedPageId: this._selectedPageId,
|
|
601
|
+
pageUrl: pageInfo.page.url(),
|
|
602
|
+
});
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async readClipboardPayload() {
|
|
607
|
+
const pageInfo = this.getSelectedPageInfo();
|
|
608
|
+
if (!pageInfo) {
|
|
609
|
+
this.log("â ī¸ Cannot read clipboard - no active page", { selectedPageId: this._selectedPageId });
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
await pageInfo.page
|
|
614
|
+
.context()
|
|
615
|
+
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
616
|
+
.catch((error) => {
|
|
617
|
+
this.log("â ī¸ Failed to ensure clipboard permissions before read", { error });
|
|
618
|
+
});
|
|
619
|
+
const payload = await pageInfo.page.evaluate(async () => {
|
|
620
|
+
const result = {};
|
|
621
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
const arrayBufferToBase64 = (buffer) => {
|
|
625
|
+
let binary = "";
|
|
626
|
+
const bytes = new Uint8Array(buffer);
|
|
627
|
+
const chunkSize = 0x8000;
|
|
628
|
+
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
629
|
+
const chunk = bytes.subarray(index, index + chunkSize);
|
|
630
|
+
binary += String.fromCharCode(...chunk);
|
|
565
631
|
}
|
|
566
|
-
return
|
|
632
|
+
return btoa(binary);
|
|
567
633
|
};
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if (Array.isArray(clipboardPayload?.files)) {
|
|
586
|
-
for (const filePayload of clipboardPayload.files) {
|
|
587
|
-
const file = createFileFromPayload(filePayload);
|
|
588
|
-
if (file) {
|
|
634
|
+
const files = [];
|
|
635
|
+
if (typeof navigator.clipboard.read === "function") {
|
|
636
|
+
try {
|
|
637
|
+
const items = await navigator.clipboard.read();
|
|
638
|
+
for (const item of items) {
|
|
639
|
+
if (item.types.includes("text/html") && !result.html) {
|
|
640
|
+
const blob = await item.getType("text/html");
|
|
641
|
+
result.html = await blob.text();
|
|
642
|
+
}
|
|
643
|
+
if (item.types.includes("text/plain") && !result.text) {
|
|
644
|
+
const blob = await item.getType("text/plain");
|
|
645
|
+
result.text = await blob.text();
|
|
646
|
+
}
|
|
647
|
+
for (const type of item.types) {
|
|
648
|
+
if (type.startsWith("text/")) {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
589
651
|
try {
|
|
590
|
-
|
|
652
|
+
const blob = await item.getType(type);
|
|
653
|
+
const buffer = await blob.arrayBuffer();
|
|
654
|
+
files.push({
|
|
655
|
+
name: `clipboard-file-${files.length + 1}`,
|
|
656
|
+
type,
|
|
657
|
+
lastModified: Date.now(),
|
|
658
|
+
data: arrayBufferToBase64(buffer),
|
|
659
|
+
});
|
|
591
660
|
}
|
|
592
661
|
catch (error) {
|
|
593
|
-
console.warn("
|
|
662
|
+
console.warn("[RemoteClipboard] Failed to serialize clipboard blob", { type, error });
|
|
594
663
|
}
|
|
595
664
|
}
|
|
596
665
|
}
|
|
597
666
|
}
|
|
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
667
|
catch (error) {
|
|
614
|
-
console.warn("
|
|
668
|
+
console.warn("[RemoteClipboard] navigator.clipboard.read failed", error);
|
|
615
669
|
}
|
|
616
670
|
}
|
|
617
|
-
if (
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
if (clipboardPayload?.html) {
|
|
621
|
-
const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
|
|
622
|
-
if (inserted) {
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
671
|
+
if (!result.text && typeof navigator.clipboard.readText === "function") {
|
|
625
672
|
try {
|
|
626
|
-
const
|
|
627
|
-
if (
|
|
628
|
-
|
|
629
|
-
range.deleteContents();
|
|
630
|
-
const fragment = range.createContextualFragment(clipboardPayload.html);
|
|
631
|
-
range.insertNode(fragment);
|
|
632
|
-
range.collapse(false);
|
|
633
|
-
return;
|
|
673
|
+
const text = await navigator.clipboard.readText();
|
|
674
|
+
if (text) {
|
|
675
|
+
result.text = text;
|
|
634
676
|
}
|
|
635
677
|
}
|
|
636
678
|
catch (error) {
|
|
637
|
-
console.warn("
|
|
679
|
+
console.warn("[RemoteClipboard] navigator.clipboard.readText failed", error);
|
|
638
680
|
}
|
|
639
681
|
}
|
|
640
|
-
if (
|
|
641
|
-
const
|
|
642
|
-
if (
|
|
643
|
-
|
|
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);
|
|
682
|
+
if (!result.text) {
|
|
683
|
+
const selection = window.getSelection?.()?.toString?.();
|
|
684
|
+
if (selection) {
|
|
685
|
+
result.text = selection;
|
|
657
686
|
}
|
|
658
687
|
}
|
|
659
|
-
if (
|
|
660
|
-
|
|
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
|
-
}
|
|
688
|
+
if (files.length > 0) {
|
|
689
|
+
result.files = files;
|
|
676
690
|
}
|
|
677
|
-
|
|
691
|
+
return result;
|
|
692
|
+
});
|
|
693
|
+
if (this.hasClipboardData(payload)) {
|
|
694
|
+
this.log("đ Remote clipboard payload captured", {
|
|
695
|
+
hasText: !!payload.text,
|
|
696
|
+
hasHtml: !!payload.html,
|
|
697
|
+
fileCount: payload.files?.length ?? 0,
|
|
698
|
+
});
|
|
699
|
+
return payload;
|
|
700
|
+
}
|
|
701
|
+
this.log("âšī¸ Remote clipboard read returned empty payload", {
|
|
702
|
+
selectedPageId: this._selectedPageId,
|
|
703
|
+
});
|
|
704
|
+
return null;
|
|
678
705
|
}
|
|
679
706
|
catch (error) {
|
|
680
|
-
this.log("â Error
|
|
707
|
+
this.log("â Error reading remote clipboard", {
|
|
708
|
+
error: error instanceof Error ? error.message : String(error),
|
|
709
|
+
});
|
|
681
710
|
throw error;
|
|
682
711
|
}
|
|
683
712
|
}
|
|
713
|
+
// Add a helper method to verify clipboard script is loaded
|
|
714
|
+
async verifyClipboardScript(stableTabId) {
|
|
715
|
+
const pageInfo = this.pages.get(stableTabId);
|
|
716
|
+
if (!pageInfo) {
|
|
717
|
+
this.log("â ī¸ Cannot verify clipboard script - page not found", { stableTabId });
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
const isLoaded = await pageInfo.page.evaluate(() => {
|
|
722
|
+
return (typeof window.__bvt_applyClipboardData === "function" && typeof window.__bvt_reportClipboard === "function");
|
|
723
|
+
});
|
|
724
|
+
this.log("đ Clipboard script verification", {
|
|
725
|
+
stableTabId,
|
|
726
|
+
isLoaded,
|
|
727
|
+
url: pageInfo.page.url(),
|
|
728
|
+
});
|
|
729
|
+
return isLoaded;
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
this.log("â Error verifying clipboard script", {
|
|
733
|
+
stableTabId,
|
|
734
|
+
error: error instanceof Error ? error.message : String(error),
|
|
735
|
+
});
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// Call this after navigation to ensure script is loaded
|
|
740
|
+
async ensureClipboardScriptLoaded(page) {
|
|
741
|
+
try {
|
|
742
|
+
const isLoaded = await page.evaluate(() => {
|
|
743
|
+
return typeof window.__bvt_applyClipboardData === "function";
|
|
744
|
+
});
|
|
745
|
+
if (!isLoaded) {
|
|
746
|
+
this.log("â ī¸ Clipboard script not loaded, attempting to reload");
|
|
747
|
+
// The script should be in initScripts, so it will load on next navigation
|
|
748
|
+
// or you could manually inject it here
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
this.log("â
Clipboard script confirmed loaded");
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
this.log("â Error checking clipboard script", { error });
|
|
756
|
+
}
|
|
757
|
+
}
|
|
684
758
|
getSelectedPage() {
|
|
685
|
-
const pageInfo = this.
|
|
759
|
+
const pageInfo = this.getSelectedPageInfo();
|
|
686
760
|
this.log("đ Getting selected page", {
|
|
687
761
|
selectedPageId: this._selectedPageId,
|
|
688
762
|
found: !!pageInfo,
|