@dev-blinq/cucumber_client 1.0.1428-dev → 1.0.1428-stage
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/assets/bundled_scripts/recorder.js +73 -73
- package/bin/assets/preload/css_gen.js +10 -10
- package/bin/assets/preload/toolbar.js +27 -29
- package/bin/assets/preload/unique_locators.js +1 -1
- package/bin/assets/preload/yaml.js +288 -275
- package/bin/assets/scripts/aria_snapshot.js +223 -220
- package/bin/assets/scripts/dom_attr.js +329 -329
- package/bin/assets/scripts/dom_parent.js +169 -174
- package/bin/assets/scripts/event_utils.js +94 -94
- package/bin/assets/scripts/pw.js +2050 -1949
- package/bin/assets/scripts/recorder.js +70 -45
- package/bin/assets/scripts/snapshot_capturer.js +147 -147
- package/bin/assets/scripts/unique_locators.js +170 -49
- package/bin/assets/scripts/yaml.js +796 -783
- package/bin/assets/templates/_hooks_template.txt +6 -2
- package/bin/assets/templates/utils_template.txt +16 -16
- package/bin/client/code_cleanup/find_step_definition_references.js +0 -1
- package/bin/client/code_gen/api_codegen.js +2 -2
- package/bin/client/code_gen/code_inversion.js +63 -2
- package/bin/client/code_gen/function_signature.js +4 -0
- package/bin/client/code_gen/page_reflection.js +52 -11
- package/bin/client/code_gen/playwright_codeget.js +25 -3
- package/bin/client/cucumber/feature_data.js +2 -2
- package/bin/client/cucumber/project_to_document.js +8 -2
- package/bin/client/cucumber/steps_definitions.js +19 -3
- package/bin/client/local_agent.js +3 -2
- package/bin/client/parse_feature_file.js +23 -26
- package/bin/client/playground/projects/env.json +2 -2
- package/bin/client/recorderv3/bvt_init.js +363 -0
- package/bin/client/recorderv3/bvt_recorder.js +1009 -47
- package/bin/client/recorderv3/implemented_steps.js +2 -0
- package/bin/client/recorderv3/index.js +3 -283
- package/bin/client/recorderv3/scriptTest.js +1 -1
- package/bin/client/recorderv3/services.js +818 -142
- package/bin/client/recorderv3/step_runner.js +28 -8
- package/bin/client/recorderv3/step_utils.js +511 -39
- package/bin/client/recorderv3/update_feature.js +32 -13
- package/bin/client/recorderv3/wbr_entry.js +61 -0
- package/bin/client/recording.js +1 -0
- package/bin/client/upload-service.js +4 -2
- package/bin/client/utils/socket_logger.js +1 -1
- package/bin/index.js +4 -1
- package/package.json +6 -4
|
@@ -4,20 +4,330 @@ import { existsSync, readdirSync, readFileSync, rmSync } from "fs";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import url from "url";
|
|
6
6
|
import { getImplementedSteps, parseRouteFiles } from "./implemented_steps.js";
|
|
7
|
-
import { NamesService } from "./services.js";
|
|
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";
|
|
15
14
|
import { unEscapeNonPrintables } from "../cucumber/utils.js";
|
|
16
15
|
import { findAvailablePort } from "../utils/index.js";
|
|
17
16
|
import socketLogger from "../utils/socket_logger.js";
|
|
17
|
+
import { tmpdir } from "os";
|
|
18
|
+
import { faker } from "@faker-js/faker/locale/en_US";
|
|
19
|
+
import { chromium } from "playwright-core";
|
|
20
|
+
|
|
18
21
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
19
22
|
|
|
20
23
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
|
|
25
|
+
const clipboardBridgeScript = `
|
|
26
|
+
;(() => {
|
|
27
|
+
if (window.__bvtRecorderClipboardBridgeInitialized) {
|
|
28
|
+
console.log('[ClipboardBridge] Already initialized, skipping');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
window.__bvtRecorderClipboardBridgeInitialized = true;
|
|
32
|
+
console.log('[ClipboardBridge] Initializing clipboard bridge');
|
|
33
|
+
|
|
34
|
+
const emitPayload = (payload, attempt = 0) => {
|
|
35
|
+
const reporter = window.__bvt_reportClipboard;
|
|
36
|
+
if (typeof reporter === "function") {
|
|
37
|
+
try {
|
|
38
|
+
console.log('[ClipboardBridge] Reporting clipboard payload:', payload);
|
|
39
|
+
reporter(payload);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.warn("[ClipboardBridge] Failed to report payload", error);
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (attempt < 5) {
|
|
46
|
+
console.log('[ClipboardBridge] Reporter not ready, retrying...', attempt);
|
|
47
|
+
setTimeout(() => emitPayload(payload, attempt + 1), 50 * (attempt + 1));
|
|
48
|
+
} else {
|
|
49
|
+
console.warn('[ClipboardBridge] Reporter never became available');
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const fileToBase64 = (file) => {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
try {
|
|
56
|
+
const reader = new FileReader();
|
|
57
|
+
reader.onload = () => {
|
|
58
|
+
const { result } = reader;
|
|
59
|
+
if (typeof result === "string") {
|
|
60
|
+
const index = result.indexOf("base64,");
|
|
61
|
+
resolve(index !== -1 ? result.substring(index + 7) : result);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (result instanceof ArrayBuffer) {
|
|
65
|
+
const bytes = new Uint8Array(result);
|
|
66
|
+
let binary = "";
|
|
67
|
+
const chunk = 0x8000;
|
|
68
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
69
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
70
|
+
}
|
|
71
|
+
resolve(btoa(binary));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
resolve(null);
|
|
75
|
+
};
|
|
76
|
+
reader.onerror = () => resolve(null);
|
|
77
|
+
reader.readAsDataURL(file);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.warn("[ClipboardBridge] Failed to serialize file", error);
|
|
80
|
+
resolve(null);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handleClipboardEvent = async (event) => {
|
|
86
|
+
try {
|
|
87
|
+
console.log('[ClipboardBridge] Handling clipboard event:', event.type);
|
|
88
|
+
const payload = { trigger: event.type };
|
|
89
|
+
const clipboardData = event.clipboardData;
|
|
90
|
+
|
|
91
|
+
if (clipboardData) {
|
|
92
|
+
try {
|
|
93
|
+
const text = clipboardData.getData("text/plain");
|
|
94
|
+
if (text) {
|
|
95
|
+
payload.text = text;
|
|
96
|
+
console.log('[ClipboardBridge] Captured text:', text.substring(0, 50));
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.warn("[ClipboardBridge] Could not read text/plain", error);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const html = clipboardData.getData("text/html");
|
|
104
|
+
if (html) {
|
|
105
|
+
payload.html = html;
|
|
106
|
+
console.log('[ClipboardBridge] Captured HTML:', html.substring(0, 50));
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.warn("[ClipboardBridge] Could not read text/html", error);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const files = clipboardData.files;
|
|
113
|
+
if (files && files.length > 0) {
|
|
114
|
+
console.log('[ClipboardBridge] Processing files:', files.length);
|
|
115
|
+
const serialized = [];
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
const data = await fileToBase64(file);
|
|
118
|
+
if (data) {
|
|
119
|
+
serialized.push({
|
|
120
|
+
name: file.name,
|
|
121
|
+
type: file.type,
|
|
122
|
+
lastModified: file.lastModified,
|
|
123
|
+
data,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (serialized.length > 0) {
|
|
128
|
+
payload.files = serialized;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!payload.text) {
|
|
134
|
+
try {
|
|
135
|
+
const selection = window.getSelection?.();
|
|
136
|
+
const selectionText = selection?.toString?.();
|
|
137
|
+
if (selectionText) {
|
|
138
|
+
payload.text = selectionText;
|
|
139
|
+
console.log('[ClipboardBridge] Using selection text:', selectionText.substring(0, 50));
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// Ignore selection access errors.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
emitPayload(payload);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.warn("[ClipboardBridge] Could not process event", error);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// NEW: Function to apply clipboard data to the page
|
|
153
|
+
window.__bvt_applyClipboardData = (payload) => {
|
|
154
|
+
console.log('[ClipboardBridge] Applying clipboard data:', payload);
|
|
155
|
+
|
|
156
|
+
if (!payload) {
|
|
157
|
+
console.warn('[ClipboardBridge] No payload provided');
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Create DataTransfer object
|
|
163
|
+
let dataTransfer = null;
|
|
164
|
+
try {
|
|
165
|
+
dataTransfer = new DataTransfer();
|
|
166
|
+
console.log('[ClipboardBridge] DataTransfer created');
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.warn('[ClipboardBridge] Could not create DataTransfer', error);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (dataTransfer) {
|
|
172
|
+
if (payload.text) {
|
|
173
|
+
try {
|
|
174
|
+
dataTransfer.setData("text/plain", payload.text);
|
|
175
|
+
console.log('[ClipboardBridge] Set text/plain:', payload.text.substring(0, 50));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.warn('[ClipboardBridge] Failed to set text/plain', error);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (payload.html) {
|
|
181
|
+
try {
|
|
182
|
+
dataTransfer.setData("text/html", payload.html);
|
|
183
|
+
console.log('[ClipboardBridge] Set text/html:', payload.html.substring(0, 50));
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.warn('[ClipboardBridge] Failed to set text/html', error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Get target element
|
|
191
|
+
let target = document.activeElement || document.body;
|
|
192
|
+
console.log('[ClipboardBridge] Target element:', {
|
|
193
|
+
tagName: target.tagName,
|
|
194
|
+
type: target.type,
|
|
195
|
+
isContentEditable: target.isContentEditable,
|
|
196
|
+
id: target.id,
|
|
197
|
+
className: target.className
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Try synthetic paste event first
|
|
201
|
+
let pasteHandled = false;
|
|
202
|
+
if (dataTransfer && target && typeof target.dispatchEvent === "function") {
|
|
203
|
+
try {
|
|
204
|
+
const pasteEvent = new ClipboardEvent("paste", {
|
|
205
|
+
clipboardData: dataTransfer,
|
|
206
|
+
bubbles: true,
|
|
207
|
+
cancelable: true,
|
|
208
|
+
});
|
|
209
|
+
pasteHandled = target.dispatchEvent(pasteEvent);
|
|
210
|
+
console.log('[ClipboardBridge] Paste event dispatched, handled:', pasteHandled);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.warn('[ClipboardBridge] Failed to dispatch paste event', error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
console.log('[ClipboardBridge] Paste event not handled, trying fallback methods');
|
|
218
|
+
|
|
219
|
+
// Fallback: Try execCommand with HTML first (for contenteditable)
|
|
220
|
+
if (payload.html && target.isContentEditable) {
|
|
221
|
+
console.log('[ClipboardBridge] Trying execCommand insertHTML');
|
|
222
|
+
try {
|
|
223
|
+
const inserted = document.execCommand('insertHTML', false, payload.html);
|
|
224
|
+
if (inserted) {
|
|
225
|
+
console.log('[ClipboardBridge] Successfully inserted HTML via execCommand');
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.warn('[ClipboardBridge] execCommand insertHTML failed', error);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Try Range API for HTML
|
|
233
|
+
console.log('[ClipboardBridge] Trying Range API for HTML');
|
|
234
|
+
try {
|
|
235
|
+
const selection = window.getSelection?.();
|
|
236
|
+
if (selection && selection.rangeCount > 0) {
|
|
237
|
+
const range = selection.getRangeAt(0);
|
|
238
|
+
range.deleteContents();
|
|
239
|
+
const fragment = range.createContextualFragment(payload.html);
|
|
240
|
+
range.insertNode(fragment);
|
|
241
|
+
range.collapse(false);
|
|
242
|
+
console.log('[ClipboardBridge] Successfully inserted HTML via Range API');
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.warn('[ClipboardBridge] Range API HTML insertion failed', error);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Fallback: Try execCommand with text
|
|
251
|
+
if (payload.text) {
|
|
252
|
+
console.log('[ClipboardBridge] Trying execCommand insertText');
|
|
253
|
+
try {
|
|
254
|
+
const inserted = document.execCommand('insertText', false, payload.text);
|
|
255
|
+
if (inserted) {
|
|
256
|
+
console.log('[ClipboardBridge] Successfully inserted text via execCommand');
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.warn('[ClipboardBridge] execCommand insertText failed', error);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Try Range API for text
|
|
264
|
+
if (target.isContentEditable) {
|
|
265
|
+
console.log('[ClipboardBridge] Trying Range API for text');
|
|
266
|
+
try {
|
|
267
|
+
const selection = window.getSelection?.();
|
|
268
|
+
if (selection && selection.rangeCount > 0) {
|
|
269
|
+
const range = selection.getRangeAt(0);
|
|
270
|
+
range.deleteContents();
|
|
271
|
+
range.insertNode(document.createTextNode(payload.text));
|
|
272
|
+
range.collapse(false);
|
|
273
|
+
console.log('[ClipboardBridge] Successfully inserted text via Range API');
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.warn('[ClipboardBridge] Range API text insertion failed', error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Last resort: Direct value assignment for input/textarea
|
|
282
|
+
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
|
|
283
|
+
console.log('[ClipboardBridge] Trying direct value assignment');
|
|
284
|
+
try {
|
|
285
|
+
const start = target.selectionStart ?? target.value.length ?? 0;
|
|
286
|
+
const end = target.selectionEnd ?? target.value.length ?? 0;
|
|
287
|
+
const value = target.value ?? "";
|
|
288
|
+
const text = payload.text;
|
|
289
|
+
target.value = value.slice(0, start) + text + value.slice(end);
|
|
290
|
+
const caret = start + text.length;
|
|
291
|
+
if (typeof target.setSelectionRange === 'function') {
|
|
292
|
+
target.setSelectionRange(caret, caret);
|
|
293
|
+
}
|
|
294
|
+
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
295
|
+
console.log('[ClipboardBridge] Successfully set value directly');
|
|
296
|
+
return true;
|
|
297
|
+
} catch (error) {
|
|
298
|
+
console.warn('[ClipboardBridge] Direct value assignment failed', error);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
console.warn('[ClipboardBridge] All paste methods failed');
|
|
304
|
+
return false;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error('[ClipboardBridge] Error applying clipboard data:', error);
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Set up event listeners for copy/cut
|
|
312
|
+
document.addEventListener(
|
|
313
|
+
"copy",
|
|
314
|
+
(event) => {
|
|
315
|
+
void handleClipboardEvent(event);
|
|
316
|
+
},
|
|
317
|
+
true
|
|
318
|
+
);
|
|
319
|
+
document.addEventListener(
|
|
320
|
+
"cut",
|
|
321
|
+
(event) => {
|
|
322
|
+
void handleClipboardEvent(event);
|
|
323
|
+
},
|
|
324
|
+
true
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
console.log('[ClipboardBridge] Clipboard bridge initialized successfully');
|
|
328
|
+
})();
|
|
329
|
+
`;
|
|
330
|
+
|
|
21
331
|
export function getInitScript(config, options) {
|
|
22
332
|
const preScript = `
|
|
23
333
|
window.__bvt_Recorder_config = ${JSON.stringify(config ?? null)};
|
|
@@ -27,7 +337,7 @@ export function getInitScript(config, options) {
|
|
|
27
337
|
path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"),
|
|
28
338
|
"utf8"
|
|
29
339
|
);
|
|
30
|
-
return preScript + recorderScript;
|
|
340
|
+
return preScript + recorderScript + clipboardBridgeScript;
|
|
31
341
|
}
|
|
32
342
|
|
|
33
343
|
async function evaluate(frame, script) {
|
|
@@ -154,6 +464,17 @@ const transformAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode,
|
|
|
154
464
|
}
|
|
155
465
|
}
|
|
156
466
|
};
|
|
467
|
+
const diffPaths = (currentPath, newPath) => {
|
|
468
|
+
const currentDomain = new URL(currentPath).hostname;
|
|
469
|
+
const newDomain = new URL(newPath).hostname;
|
|
470
|
+
if (currentDomain !== newDomain) {
|
|
471
|
+
return true;
|
|
472
|
+
} else {
|
|
473
|
+
const currentRoute = new URL(currentPath).pathname;
|
|
474
|
+
const newRoute = new URL(newPath).pathname;
|
|
475
|
+
return currentRoute !== newRoute;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
157
478
|
/**
|
|
158
479
|
* @typedef {Object} BVTRecorderInput
|
|
159
480
|
* @property {string} envName
|
|
@@ -183,11 +504,19 @@ export class BVTRecorder {
|
|
|
183
504
|
projectDir: this.projectDir,
|
|
184
505
|
logger: this.logger,
|
|
185
506
|
});
|
|
507
|
+
this.workspaceService = new PublishService(this.TOKEN);
|
|
186
508
|
this.pageSet = new Set();
|
|
187
509
|
this.lastKnownUrlPath = "";
|
|
188
510
|
this.world = { attach: () => {} };
|
|
189
511
|
this.shouldTakeScreenshot = true;
|
|
190
512
|
this.watcher = null;
|
|
513
|
+
this.networkEventsFolder = path.join(tmpdir(), "blinq_network_events");
|
|
514
|
+
this.tempProjectFolder = `${tmpdir()}/bvt_temp_project_${Math.floor(Math.random() * 1000000)}`;
|
|
515
|
+
this.tempSnapshotsFolder = path.join(this.tempProjectFolder, "data/snapshots");
|
|
516
|
+
|
|
517
|
+
if (existsSync(this.networkEventsFolder)) {
|
|
518
|
+
rmSync(this.networkEventsFolder, { recursive: true, force: true });
|
|
519
|
+
}
|
|
191
520
|
}
|
|
192
521
|
events = {
|
|
193
522
|
onFrameNavigate: "BVTRecorder.onFrameNavigate",
|
|
@@ -202,6 +531,12 @@ export class BVTRecorder {
|
|
|
202
531
|
cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
|
|
203
532
|
cmdExecutionError: "BVTRecorder.cmdExecutionError",
|
|
204
533
|
interceptResults: "BVTRecorder.interceptResults",
|
|
534
|
+
onDebugURLChange: "BVTRecorder.onDebugURLChange",
|
|
535
|
+
updateCommand: "BVTRecorder.updateCommand",
|
|
536
|
+
browserStateSync: "BrowserService.stateSync",
|
|
537
|
+
browserStateError: "BrowserService.stateError",
|
|
538
|
+
clipboardPush: "BrowserService.clipboardPush",
|
|
539
|
+
clipboardError: "BrowserService.clipboardError",
|
|
205
540
|
};
|
|
206
541
|
bindings = {
|
|
207
542
|
__bvt_recordCommand: async ({ frame, page, context }, event) => {
|
|
@@ -231,6 +566,25 @@ export class BVTRecorder {
|
|
|
231
566
|
__bvt_getObject: (_src, obj) => {
|
|
232
567
|
this.processObject(obj);
|
|
233
568
|
},
|
|
569
|
+
__bvt_reportClipboard: async ({ page }, payload) => {
|
|
570
|
+
try {
|
|
571
|
+
if (!payload) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
575
|
+
if (activePage && activePage !== page) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const pageUrl = typeof page?.url === "function" ? page.url() : null;
|
|
579
|
+
this.sendEvent(this.events.clipboardPush, {
|
|
580
|
+
data: payload,
|
|
581
|
+
trigger: payload?.trigger ?? "copy",
|
|
582
|
+
pageUrl,
|
|
583
|
+
});
|
|
584
|
+
} catch (error) {
|
|
585
|
+
this.logger.error("Error forwarding clipboard payload from page", error);
|
|
586
|
+
}
|
|
587
|
+
},
|
|
234
588
|
};
|
|
235
589
|
|
|
236
590
|
getSnapshot = async (attr) => {
|
|
@@ -288,8 +642,12 @@ export class BVTRecorder {
|
|
|
288
642
|
}
|
|
289
643
|
|
|
290
644
|
async _initBrowser({ url }) {
|
|
291
|
-
|
|
292
|
-
|
|
645
|
+
if (process.env.CDP_LISTEN_PORT === undefined) {
|
|
646
|
+
this.#remoteDebuggerPort = await findAvailablePort();
|
|
647
|
+
process.env.CDP_LISTEN_PORT = this.#remoteDebuggerPort;
|
|
648
|
+
} else {
|
|
649
|
+
this.#remoteDebuggerPort = process.env.CDP_LISTEN_PORT;
|
|
650
|
+
}
|
|
293
651
|
|
|
294
652
|
// this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
|
|
295
653
|
this.world = { attach: () => {} };
|
|
@@ -312,7 +670,8 @@ export class BVTRecorder {
|
|
|
312
670
|
],
|
|
313
671
|
};
|
|
314
672
|
|
|
315
|
-
const
|
|
673
|
+
const scenario = { pickle: this.scenarioDoc };
|
|
674
|
+
const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName, scenario);
|
|
316
675
|
this.bvtContext = bvtContext;
|
|
317
676
|
this.stepRunner = new BVTStepRunner({
|
|
318
677
|
projectDir: this.projectDir,
|
|
@@ -338,12 +697,22 @@ export class BVTRecorder {
|
|
|
338
697
|
},
|
|
339
698
|
bvtContext: this.bvtContext,
|
|
340
699
|
});
|
|
341
|
-
|
|
342
|
-
this.context = context;
|
|
700
|
+
this.context = bvtContext.playContext;
|
|
343
701
|
this.web = bvtContext.stable || bvtContext.web;
|
|
344
702
|
this.web.tryAllStrategies = true;
|
|
345
703
|
this.page = bvtContext.page;
|
|
346
704
|
this.pageSet.add(this.page);
|
|
705
|
+
if (process.env.REMOTE_RECORDER === "true") {
|
|
706
|
+
this.browserEmitter = new RemoteBrowserService({
|
|
707
|
+
CDP_CONNECT_URL: `http://localhost:${this.#remoteDebuggerPort}`,
|
|
708
|
+
context: this.context,
|
|
709
|
+
});
|
|
710
|
+
this.browserEmitter.on(this.events.browserStateSync, (state) => {
|
|
711
|
+
this.page = this.browserEmitter.getSelectedPage();
|
|
712
|
+
this.sendEvent(this.events.browserStateSync, state);
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
347
716
|
this.lastKnownUrlPath = this._updateUrlPath();
|
|
348
717
|
const browser = await this.context.browser();
|
|
349
718
|
this.browser = browser;
|
|
@@ -356,6 +725,14 @@ export class BVTRecorder {
|
|
|
356
725
|
this.web.onRestoreSaveState = (url) => {
|
|
357
726
|
this._initBrowser({ url });
|
|
358
727
|
};
|
|
728
|
+
|
|
729
|
+
// create a second browser for locator generation
|
|
730
|
+
this.backgroundBrowser = await chromium.launch({
|
|
731
|
+
headless: true,
|
|
732
|
+
});
|
|
733
|
+
this.backgroundContext = await this.backgroundBrowser.newContext({});
|
|
734
|
+
await this.backgroundContext.addInitScript({ content: this.getInitScripts(this.config) });
|
|
735
|
+
await this.backgroundContext.newPage();
|
|
359
736
|
}
|
|
360
737
|
async onClosePopup() {
|
|
361
738
|
// console.log("close popups");
|
|
@@ -397,13 +774,14 @@ export class BVTRecorder {
|
|
|
397
774
|
|
|
398
775
|
await this.page.goto(url, {
|
|
399
776
|
waitUntil: "domcontentloaded",
|
|
777
|
+
timeout: this.config.page_timeout ?? 60_000,
|
|
400
778
|
});
|
|
401
779
|
// add listener for frame navigation on current tab
|
|
402
780
|
this._addFrameNavigateListener(this.page);
|
|
403
781
|
|
|
404
782
|
// eval init script on current tab
|
|
405
783
|
// await this._initPage(this.page);
|
|
406
|
-
this.#currentURL =
|
|
784
|
+
this.#currentURL = url;
|
|
407
785
|
|
|
408
786
|
await this.page.dispatchEvent("html", "scroll");
|
|
409
787
|
await delay(1000);
|
|
@@ -445,14 +823,15 @@ export class BVTRecorder {
|
|
|
445
823
|
element: { inputID: "frame" },
|
|
446
824
|
});
|
|
447
825
|
|
|
448
|
-
const
|
|
826
|
+
const newUrl = frame.url();
|
|
827
|
+
const newPath = new URL(newUrl).pathname;
|
|
449
828
|
const newTitle = await frame.title();
|
|
450
|
-
|
|
829
|
+
const changed = diffPaths(this.#currentURL, newUrl);
|
|
830
|
+
|
|
831
|
+
if (changed) {
|
|
451
832
|
this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
|
|
452
|
-
this.#currentURL =
|
|
833
|
+
this.#currentURL = newUrl;
|
|
453
834
|
}
|
|
454
|
-
// await this._setRecordingMode(frame);
|
|
455
|
-
// await this._initPage(page);
|
|
456
835
|
} catch (error) {
|
|
457
836
|
this.logger.error("Error in frame navigate event");
|
|
458
837
|
this.logger.error(error);
|
|
@@ -617,7 +996,6 @@ export class BVTRecorder {
|
|
|
617
996
|
try {
|
|
618
997
|
if (page.isClosed()) return;
|
|
619
998
|
this.pageSet.add(page);
|
|
620
|
-
|
|
621
999
|
await page.waitForLoadState("domcontentloaded");
|
|
622
1000
|
|
|
623
1001
|
// add listener for frame navigation on new tab
|
|
@@ -682,6 +1060,52 @@ export class BVTRecorder {
|
|
|
682
1060
|
console.error("Error in saving screenshot: ", error);
|
|
683
1061
|
}
|
|
684
1062
|
}
|
|
1063
|
+
async generateLocators(event) {
|
|
1064
|
+
const snapshotDetails = event.snapshotDetails;
|
|
1065
|
+
if (!snapshotDetails) {
|
|
1066
|
+
throw new Error("No snapshot details found");
|
|
1067
|
+
}
|
|
1068
|
+
const mode = event.mode;
|
|
1069
|
+
const inputID = event.element.inputID;
|
|
1070
|
+
|
|
1071
|
+
const { id, contextId, doc } = snapshotDetails;
|
|
1072
|
+
// const selector = `[data-blinq-id="${id}"]`;
|
|
1073
|
+
const newPage = await this.backgroundContext.newPage();
|
|
1074
|
+
await newPage.setContent(doc, { waitUntil: "domcontentloaded" });
|
|
1075
|
+
const locatorsObj = await newPage.evaluate(
|
|
1076
|
+
([id, contextId, mode]) => {
|
|
1077
|
+
const recorder = window.__bvt_Recorder;
|
|
1078
|
+
const contextElement = document.querySelector(`[data-blinq-context-id="${contextId}"]`);
|
|
1079
|
+
const el = document.querySelector(`[data-blinq-id="${id}"]`);
|
|
1080
|
+
if (contextElement) {
|
|
1081
|
+
const result = recorder.locatorGenerator.toContextLocators(el, contextElement);
|
|
1082
|
+
return result;
|
|
1083
|
+
}
|
|
1084
|
+
const isRecordingText = mode === "recordingText";
|
|
1085
|
+
return recorder.locatorGenerator.getElementLocators(el, {
|
|
1086
|
+
excludeText: isRecordingText,
|
|
1087
|
+
});
|
|
1088
|
+
},
|
|
1089
|
+
[id, contextId, mode]
|
|
1090
|
+
);
|
|
1091
|
+
|
|
1092
|
+
// console.log(`Generated locators: for ${inputID}: `, JSON.stringify(locatorsObj));
|
|
1093
|
+
await newPage.close();
|
|
1094
|
+
if (event.nestFrmLoc?.children) {
|
|
1095
|
+
locatorsObj.nestFrmLoc = event.nestFrmLoc.children;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
this.sendEvent(this.events.updateCommand, {
|
|
1099
|
+
locators: {
|
|
1100
|
+
locators: locatorsObj.locators,
|
|
1101
|
+
nestFrmLoc: locatorsObj.nestFrmLoc,
|
|
1102
|
+
iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
1103
|
+
},
|
|
1104
|
+
allStrategyLocators: locatorsObj.allStrategyLocators,
|
|
1105
|
+
inputID,
|
|
1106
|
+
});
|
|
1107
|
+
// const
|
|
1108
|
+
}
|
|
685
1109
|
async onAction(event) {
|
|
686
1110
|
this._updateUrlPath();
|
|
687
1111
|
// const locators = this.overlayLocators(event);
|
|
@@ -695,25 +1119,26 @@ export class BVTRecorder {
|
|
|
695
1119
|
event.mode === "recordingHover",
|
|
696
1120
|
event.mode === "multiInspecting"
|
|
697
1121
|
),
|
|
698
|
-
locators: {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
},
|
|
702
|
-
allStrategyLocators: event.allStrategyLocators,
|
|
1122
|
+
// locators: {
|
|
1123
|
+
// locators: event.locators,
|
|
1124
|
+
// iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
1125
|
+
// },
|
|
1126
|
+
// allStrategyLocators: event.allStrategyLocators,
|
|
703
1127
|
url: event.frame.url,
|
|
704
1128
|
title: event.frame.title,
|
|
705
1129
|
extract: {},
|
|
706
1130
|
lastKnownUrlPath: this.lastKnownUrlPath,
|
|
707
1131
|
};
|
|
708
|
-
if (event.nestFrmLoc?.children) {
|
|
709
|
-
|
|
710
|
-
}
|
|
1132
|
+
// if (event.nestFrmLoc?.children) {
|
|
1133
|
+
// cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
|
|
1134
|
+
// }
|
|
711
1135
|
// this.logger.info({ event });
|
|
712
1136
|
if (this.shouldTakeScreenshot) {
|
|
713
1137
|
await this.storeScreenshot(event);
|
|
714
1138
|
}
|
|
715
1139
|
this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
716
1140
|
this._updateUrlPath();
|
|
1141
|
+
await this.generateLocators(event);
|
|
717
1142
|
}
|
|
718
1143
|
_updateUrlPath() {
|
|
719
1144
|
try {
|
|
@@ -735,7 +1160,6 @@ export class BVTRecorder {
|
|
|
735
1160
|
this.previousHistoryLength = null;
|
|
736
1161
|
this.previousUrl = null;
|
|
737
1162
|
this.previousEntries = null;
|
|
738
|
-
|
|
739
1163
|
await closeContext();
|
|
740
1164
|
this.pageSet.clear();
|
|
741
1165
|
}
|
|
@@ -758,25 +1182,26 @@ export class BVTRecorder {
|
|
|
758
1182
|
for (let i = 0; i < 3; i++) {
|
|
759
1183
|
result = 0;
|
|
760
1184
|
try {
|
|
761
|
-
for (const page of this.context.pages()) {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1185
|
+
// for (const page of this.context.pages()) {
|
|
1186
|
+
const page = this.web.page;
|
|
1187
|
+
for (const frame of page.frames()) {
|
|
1188
|
+
try {
|
|
1189
|
+
//scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params: Params)
|
|
1190
|
+
const frameResult = await this.web._locateElementByText(
|
|
1191
|
+
frame,
|
|
1192
|
+
searchString,
|
|
1193
|
+
tag,
|
|
1194
|
+
regex,
|
|
1195
|
+
partial,
|
|
1196
|
+
ignoreCase,
|
|
1197
|
+
{}
|
|
1198
|
+
);
|
|
1199
|
+
result += frameResult.elementCount;
|
|
1200
|
+
} catch (e) {
|
|
1201
|
+
console.log(e);
|
|
778
1202
|
}
|
|
779
1203
|
}
|
|
1204
|
+
// }
|
|
780
1205
|
|
|
781
1206
|
return result;
|
|
782
1207
|
} catch (e) {
|
|
@@ -829,17 +1254,27 @@ export class BVTRecorder {
|
|
|
829
1254
|
}
|
|
830
1255
|
async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
|
|
831
1256
|
const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
|
|
1257
|
+
|
|
1258
|
+
const env = path.basename(this.envName, ".json");
|
|
832
1259
|
const _env = {
|
|
833
1260
|
TOKEN: this.TOKEN,
|
|
834
1261
|
TEMP_RUN: true,
|
|
835
1262
|
REPORT_FOLDER: this.bvtContext.reportFolder,
|
|
836
1263
|
BLINQ_ENV: this.envName,
|
|
837
|
-
STORE_DETAILED_NETWORK_DATA: listenNetwork ? "true" : "false",
|
|
838
1264
|
DEBUG: "blinq:route",
|
|
1265
|
+
// BVT_TEMP_SNAPSHOTS_FOLDER: step.isImplemented ? path.join(this.tempSnapshotsFolder, env) : undefined,
|
|
839
1266
|
};
|
|
1267
|
+
if (!step.isImplemented) {
|
|
1268
|
+
_env.BVT_TEMP_SNAPSHOTS_FOLDER = path.join(this.tempSnapshotsFolder, env);
|
|
1269
|
+
}
|
|
840
1270
|
|
|
841
1271
|
this.bvtContext.navigate = true;
|
|
842
1272
|
this.bvtContext.loadedRoutes = null;
|
|
1273
|
+
if (listenNetwork) {
|
|
1274
|
+
this.bvtContext.STORE_DETAILED_NETWORK_DATA = true;
|
|
1275
|
+
} else {
|
|
1276
|
+
this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
|
|
1277
|
+
}
|
|
843
1278
|
for (const [key, value] of Object.entries(_env)) {
|
|
844
1279
|
process.env[key] = value;
|
|
845
1280
|
}
|
|
@@ -851,6 +1286,7 @@ export class BVTRecorder {
|
|
|
851
1286
|
await this.setMode("running");
|
|
852
1287
|
|
|
853
1288
|
try {
|
|
1289
|
+
step.text = step.text.trim();
|
|
854
1290
|
const { result, info } = await this.stepRunner.runStep(
|
|
855
1291
|
{
|
|
856
1292
|
step,
|
|
@@ -877,10 +1313,24 @@ export class BVTRecorder {
|
|
|
877
1313
|
this.bvtContext.navigate = false;
|
|
878
1314
|
}
|
|
879
1315
|
}
|
|
880
|
-
async saveScenario({ scenario, featureName, override, isSingleStep }) {
|
|
881
|
-
await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
|
|
882
|
-
if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
|
|
883
|
-
await this.
|
|
1316
|
+
async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env }) {
|
|
1317
|
+
// await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
|
|
1318
|
+
// if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
|
|
1319
|
+
const res = await this.workspaceService.saveScenario({
|
|
1320
|
+
scenario,
|
|
1321
|
+
featureName,
|
|
1322
|
+
override,
|
|
1323
|
+
isSingleStep,
|
|
1324
|
+
branch,
|
|
1325
|
+
isEditing,
|
|
1326
|
+
projectId: path.basename(this.projectDir),
|
|
1327
|
+
env: env ?? this.envName,
|
|
1328
|
+
});
|
|
1329
|
+
if (res.success) {
|
|
1330
|
+
await this.cleanup({ tags: scenario.tags });
|
|
1331
|
+
} else {
|
|
1332
|
+
throw new Error(res.message || "Error saving scenario");
|
|
1333
|
+
}
|
|
884
1334
|
}
|
|
885
1335
|
async getImplementedSteps() {
|
|
886
1336
|
const stepsAndScenarios = await getImplementedSteps(this.projectDir);
|
|
@@ -1045,6 +1495,7 @@ export class BVTRecorder {
|
|
|
1045
1495
|
const featureFilePath = path.join(this.projectDir, "features", featureName);
|
|
1046
1496
|
const gherkinDoc = this.parseFeatureFile(featureFilePath);
|
|
1047
1497
|
const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
|
|
1498
|
+
this.scenarioDoc = scenario;
|
|
1048
1499
|
|
|
1049
1500
|
const steps = [];
|
|
1050
1501
|
const parameters = [];
|
|
@@ -1219,4 +1670,515 @@ export class BVTRecorder {
|
|
|
1219
1670
|
}
|
|
1220
1671
|
return {};
|
|
1221
1672
|
}
|
|
1673
|
+
|
|
1674
|
+
stopRecordingNetwork(input) {
|
|
1675
|
+
if (this.bvtContext) {
|
|
1676
|
+
this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
async fakeParams(params) {
|
|
1681
|
+
const newFakeParams = {};
|
|
1682
|
+
Object.keys(params).forEach((key) => {
|
|
1683
|
+
if (!params[key].startsWith("{{") || !params[key].endsWith("}}")) {
|
|
1684
|
+
newFakeParams[key] = params[key];
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
try {
|
|
1689
|
+
const value = params[key].substring(2, params[key].length - 2).trim();
|
|
1690
|
+
const faking = value.split("(")[0].split(".");
|
|
1691
|
+
let argument = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
|
|
1692
|
+
argument = isNaN(Number(argument)) || argument === "" ? argument : Number(argument);
|
|
1693
|
+
let fakeFunc = faker;
|
|
1694
|
+
faking.forEach((f) => {
|
|
1695
|
+
fakeFunc = fakeFunc[f];
|
|
1696
|
+
});
|
|
1697
|
+
const newValue = fakeFunc(argument);
|
|
1698
|
+
newFakeParams[key] = newValue;
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
newFakeParams[key] = params[key];
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
return newFakeParams;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
async getBrowserState() {
|
|
1708
|
+
try {
|
|
1709
|
+
const state = await this.browserEmitter?.getState();
|
|
1710
|
+
this.sendEvent(this.events.browserStateSync, state);
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
this.logger.error("Error getting browser state:", error);
|
|
1713
|
+
this.sendEvent(this.events.browserStateError, {
|
|
1714
|
+
message: "Error getting browser state",
|
|
1715
|
+
code: "GET_STATE_ERROR",
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
async applyClipboardPayload(message) {
|
|
1721
|
+
const payload = message?.data ?? message;
|
|
1722
|
+
|
|
1723
|
+
this.logger.info("[BVTRecorder] applyClipboardPayload called", {
|
|
1724
|
+
hasPayload: !!payload,
|
|
1725
|
+
hasText: !!payload?.text,
|
|
1726
|
+
hasHtml: !!payload?.html,
|
|
1727
|
+
trigger: message?.trigger,
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
if (!payload) {
|
|
1731
|
+
this.logger.warn("[BVTRecorder] No payload provided");
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
try {
|
|
1736
|
+
if (this.browserEmitter && typeof this.browserEmitter.applyClipboardPayload === "function") {
|
|
1737
|
+
this.logger.info("[BVTRecorder] Using RemoteBrowserService to apply clipboard");
|
|
1738
|
+
await this.browserEmitter.applyClipboardPayload(payload);
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
1743
|
+
if (!activePage) {
|
|
1744
|
+
this.logger.warn("[BVTRecorder] No active page available");
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
this.logger.info("[BVTRecorder] Applying clipboard to page", {
|
|
1749
|
+
url: activePage.url(),
|
|
1750
|
+
isClosed: activePage.isClosed(),
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
const result = await activePage.evaluate((clipboardData) => {
|
|
1754
|
+
console.log("[Page] Executing clipboard application", clipboardData);
|
|
1755
|
+
if (typeof window.__bvt_applyClipboardData === "function") {
|
|
1756
|
+
return window.__bvt_applyClipboardData(clipboardData);
|
|
1757
|
+
}
|
|
1758
|
+
console.error("[Page] __bvt_applyClipboardData function not found!");
|
|
1759
|
+
return false;
|
|
1760
|
+
}, payload);
|
|
1761
|
+
|
|
1762
|
+
this.logger.info("[BVTRecorder] Clipboard application result:", result);
|
|
1763
|
+
|
|
1764
|
+
if (!result) {
|
|
1765
|
+
this.logger.warn("[BVTRecorder] Clipboard data not applied successfully");
|
|
1766
|
+
} else {
|
|
1767
|
+
this.logger.info("[BVTRecorder] Clipboard data applied successfully");
|
|
1768
|
+
}
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
this.logger.error("[BVTRecorder] Error applying clipboard payload", error);
|
|
1771
|
+
this.sendEvent(this.events.clipboardError, {
|
|
1772
|
+
message: "Failed to apply clipboard contents to the remote session",
|
|
1773
|
+
trigger: message?.trigger ?? "paste",
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
hasClipboardPayload(payload) {
|
|
1779
|
+
return Boolean(
|
|
1780
|
+
payload && (payload.text || payload.html || (Array.isArray(payload.files) && payload.files.length > 0))
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
async collectClipboardFromPage(page) {
|
|
1785
|
+
if (!page) {
|
|
1786
|
+
this.logger.warn("[BVTRecorder] No page available to collect clipboard data");
|
|
1787
|
+
return null;
|
|
1788
|
+
}
|
|
1789
|
+
try {
|
|
1790
|
+
await page
|
|
1791
|
+
.context()
|
|
1792
|
+
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
1793
|
+
.catch((error) => {
|
|
1794
|
+
this.logger.warn("[BVTRecorder] Failed to grant clipboard permissions before read", error);
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
const payload = await page.evaluate(async () => {
|
|
1798
|
+
const result = {};
|
|
1799
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
|
1800
|
+
return result;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
const arrayBufferToBase64 = (buffer) => {
|
|
1804
|
+
let binary = "";
|
|
1805
|
+
const bytes = new Uint8Array(buffer);
|
|
1806
|
+
const chunkSize = 0x8000;
|
|
1807
|
+
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
1808
|
+
const chunk = bytes.subarray(index, index + chunkSize);
|
|
1809
|
+
binary += String.fromCharCode(...chunk);
|
|
1810
|
+
}
|
|
1811
|
+
return btoa(binary);
|
|
1812
|
+
};
|
|
1813
|
+
|
|
1814
|
+
const files = [];
|
|
1815
|
+
|
|
1816
|
+
if (typeof navigator.clipboard.read === "function") {
|
|
1817
|
+
try {
|
|
1818
|
+
const items = await navigator.clipboard.read();
|
|
1819
|
+
for (const item of items) {
|
|
1820
|
+
if (item.types.includes("text/html") && !result.html) {
|
|
1821
|
+
const blob = await item.getType("text/html");
|
|
1822
|
+
result.html = await blob.text();
|
|
1823
|
+
}
|
|
1824
|
+
if (item.types.includes("text/plain") && !result.text) {
|
|
1825
|
+
const blob = await item.getType("text/plain");
|
|
1826
|
+
result.text = await blob.text();
|
|
1827
|
+
}
|
|
1828
|
+
for (const type of item.types) {
|
|
1829
|
+
if (type.startsWith("text/")) {
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
try {
|
|
1833
|
+
const blob = await item.getType(type);
|
|
1834
|
+
const buffer = await blob.arrayBuffer();
|
|
1835
|
+
files.push({
|
|
1836
|
+
name: `clipboard-file-${files.length + 1}`,
|
|
1837
|
+
type,
|
|
1838
|
+
lastModified: Date.now(),
|
|
1839
|
+
data: arrayBufferToBase64(buffer),
|
|
1840
|
+
});
|
|
1841
|
+
} catch (error) {
|
|
1842
|
+
console.warn("[BVTRecorder] Failed to serialize clipboard blob", { type, error });
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
} catch (error) {
|
|
1847
|
+
console.warn("[BVTRecorder] navigator.clipboard.read failed", error);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
if (!result.text && typeof navigator.clipboard.readText === "function") {
|
|
1852
|
+
try {
|
|
1853
|
+
const text = await navigator.clipboard.readText();
|
|
1854
|
+
if (text) {
|
|
1855
|
+
result.text = text;
|
|
1856
|
+
}
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
console.warn("[BVTRecorder] navigator.clipboard.readText failed", error);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
if (!result.text) {
|
|
1863
|
+
const selection = window.getSelection?.()?.toString?.();
|
|
1864
|
+
if (selection) {
|
|
1865
|
+
result.text = selection;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
if (files.length > 0) {
|
|
1870
|
+
result.files = files;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
return result;
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
return payload;
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
this.logger.error("[BVTRecorder] Error collecting clipboard payload", error);
|
|
1879
|
+
return null;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
async readClipboardPayload(message) {
|
|
1884
|
+
try {
|
|
1885
|
+
let payload = null;
|
|
1886
|
+
if (this.browserEmitter && typeof this.browserEmitter.readClipboardPayload === "function") {
|
|
1887
|
+
payload = await this.browserEmitter.readClipboardPayload();
|
|
1888
|
+
} else {
|
|
1889
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
1890
|
+
payload = await this.collectClipboardFromPage(activePage);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
if (this.hasClipboardPayload(payload)) {
|
|
1894
|
+
this.logger.info("[BVTRecorder] Remote clipboard payload ready", {
|
|
1895
|
+
hasText: !!payload.text,
|
|
1896
|
+
hasHtml: !!payload.html,
|
|
1897
|
+
files: payload.files?.length ?? 0,
|
|
1898
|
+
});
|
|
1899
|
+
this.sendEvent(this.events.clipboardPush, {
|
|
1900
|
+
data: payload,
|
|
1901
|
+
trigger: message?.trigger ?? "copy",
|
|
1902
|
+
origin: message?.source ?? "browserUI",
|
|
1903
|
+
});
|
|
1904
|
+
return payload;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
this.logger.warn("[BVTRecorder] Remote clipboard payload empty or unavailable");
|
|
1908
|
+
this.sendEvent(this.events.clipboardError, {
|
|
1909
|
+
message: "Remote clipboard is empty",
|
|
1910
|
+
trigger: message?.trigger ?? "copy",
|
|
1911
|
+
});
|
|
1912
|
+
return null;
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
this.logger.error("[BVTRecorder] Error reading clipboard payload", error);
|
|
1915
|
+
this.sendEvent(this.events.clipboardError, {
|
|
1916
|
+
message: "Failed to read clipboard contents from the remote session",
|
|
1917
|
+
trigger: message?.trigger ?? "copy",
|
|
1918
|
+
details: error instanceof Error ? error.message : String(error),
|
|
1919
|
+
});
|
|
1920
|
+
throw error;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
async injectClipboardIntoPage(page, payload) {
|
|
1925
|
+
if (!page) {
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
try {
|
|
1930
|
+
await page
|
|
1931
|
+
.context()
|
|
1932
|
+
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
1933
|
+
.catch(() => {});
|
|
1934
|
+
await page.evaluate(async (clipboardPayload) => {
|
|
1935
|
+
const toArrayBuffer = (base64) => {
|
|
1936
|
+
if (!base64) {
|
|
1937
|
+
return null;
|
|
1938
|
+
}
|
|
1939
|
+
const binaryString = atob(base64);
|
|
1940
|
+
const len = binaryString.length;
|
|
1941
|
+
const bytes = new Uint8Array(len);
|
|
1942
|
+
for (let i = 0; i < len; i += 1) {
|
|
1943
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
1944
|
+
}
|
|
1945
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
1946
|
+
};
|
|
1947
|
+
|
|
1948
|
+
const createFileFromPayload = (filePayload) => {
|
|
1949
|
+
const buffer = toArrayBuffer(filePayload?.data);
|
|
1950
|
+
if (!buffer) {
|
|
1951
|
+
return null;
|
|
1952
|
+
}
|
|
1953
|
+
const name = filePayload?.name || "clipboard-file";
|
|
1954
|
+
const type = filePayload?.type || "application/octet-stream";
|
|
1955
|
+
const lastModified = filePayload?.lastModified ?? Date.now();
|
|
1956
|
+
try {
|
|
1957
|
+
return new File([buffer], name, { type, lastModified });
|
|
1958
|
+
} catch (error) {
|
|
1959
|
+
console.warn("Clipboard bridge could not recreate File object", error);
|
|
1960
|
+
return null;
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
|
|
1964
|
+
let dataTransfer = null;
|
|
1965
|
+
try {
|
|
1966
|
+
dataTransfer = new DataTransfer();
|
|
1967
|
+
} catch (error) {
|
|
1968
|
+
console.warn("Clipboard bridge could not create DataTransfer", error);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
if (dataTransfer) {
|
|
1972
|
+
if (clipboardPayload?.text) {
|
|
1973
|
+
try {
|
|
1974
|
+
dataTransfer.setData("text/plain", clipboardPayload.text);
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
console.warn("Clipboard bridge failed to set text/plain", error);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
if (clipboardPayload?.html) {
|
|
1980
|
+
try {
|
|
1981
|
+
dataTransfer.setData("text/html", clipboardPayload.html);
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
console.warn("Clipboard bridge failed to set text/html", error);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
if (Array.isArray(clipboardPayload?.files)) {
|
|
1987
|
+
for (const filePayload of clipboardPayload.files) {
|
|
1988
|
+
const file = createFileFromPayload(filePayload);
|
|
1989
|
+
if (file) {
|
|
1990
|
+
try {
|
|
1991
|
+
dataTransfer.items.add(file);
|
|
1992
|
+
} catch (error) {
|
|
1993
|
+
console.warn("Clipboard bridge failed to append file", error);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
let target = document.activeElement || document.body;
|
|
2001
|
+
if (!target) {
|
|
2002
|
+
target = document.body || null;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
let pasteHandled = false;
|
|
2006
|
+
if (dataTransfer && target && typeof target.dispatchEvent === "function") {
|
|
2007
|
+
try {
|
|
2008
|
+
const clipboardEvent = new ClipboardEvent("paste", {
|
|
2009
|
+
clipboardData: dataTransfer,
|
|
2010
|
+
bubbles: true,
|
|
2011
|
+
cancelable: true,
|
|
2012
|
+
});
|
|
2013
|
+
pasteHandled = target.dispatchEvent(clipboardEvent);
|
|
2014
|
+
} catch (error) {
|
|
2015
|
+
console.warn("Clipboard bridge failed to dispatch synthetic paste event", error);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
if (pasteHandled) {
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
const callLegacyExecCommand = (command, value) => {
|
|
2024
|
+
const execCommand = document && document["execCommand"];
|
|
2025
|
+
if (typeof execCommand === "function") {
|
|
2026
|
+
try {
|
|
2027
|
+
return execCommand.call(document, command, false, value);
|
|
2028
|
+
} catch (error) {
|
|
2029
|
+
console.warn("Clipboard bridge failed to execute legacy command", error);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
return false;
|
|
2033
|
+
};
|
|
2034
|
+
|
|
2035
|
+
if (clipboardPayload?.html) {
|
|
2036
|
+
const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
|
|
2037
|
+
if (inserted) {
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
try {
|
|
2041
|
+
const selection = window.getSelection?.();
|
|
2042
|
+
if (selection && selection.rangeCount > 0) {
|
|
2043
|
+
const range = selection.getRangeAt(0);
|
|
2044
|
+
range.deleteContents();
|
|
2045
|
+
const fragment = range.createContextualFragment(clipboardPayload.html);
|
|
2046
|
+
range.insertNode(fragment);
|
|
2047
|
+
range.collapse(false);
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
} catch (error) {
|
|
2051
|
+
console.warn("Clipboard bridge could not insert HTML via Range APIs", error);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
if (clipboardPayload?.text) {
|
|
2056
|
+
const inserted = callLegacyExecCommand("insertText", clipboardPayload.text);
|
|
2057
|
+
if (inserted) {
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
try {
|
|
2061
|
+
const selection = window.getSelection?.();
|
|
2062
|
+
if (selection && selection.rangeCount > 0) {
|
|
2063
|
+
const range = selection.getRangeAt(0);
|
|
2064
|
+
range.deleteContents();
|
|
2065
|
+
range.insertNode(document.createTextNode(clipboardPayload.text));
|
|
2066
|
+
range.collapse(false);
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
} catch (error) {
|
|
2070
|
+
console.warn("Clipboard bridge could not insert text via Range APIs", error);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
if (clipboardPayload?.text && target && "value" in target) {
|
|
2075
|
+
try {
|
|
2076
|
+
const input = target;
|
|
2077
|
+
const start = input.selectionStart ?? input.value.length ?? 0;
|
|
2078
|
+
const end = input.selectionEnd ?? input.value.length ?? 0;
|
|
2079
|
+
const value = input.value ?? "";
|
|
2080
|
+
const text = clipboardPayload.text;
|
|
2081
|
+
input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
|
|
2082
|
+
const caret = start + text.length;
|
|
2083
|
+
if (typeof input.setSelectionRange === "function") {
|
|
2084
|
+
input.setSelectionRange(caret, caret);
|
|
2085
|
+
}
|
|
2086
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2087
|
+
} catch (error) {
|
|
2088
|
+
console.warn("Clipboard bridge failed to mutate input element", error);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
}, payload);
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
throw error;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
async createTab(url) {
|
|
2098
|
+
try {
|
|
2099
|
+
await this.browserEmitter?.createTab(url);
|
|
2100
|
+
} catch (error) {
|
|
2101
|
+
this.logger.error("Error creating tab:", error);
|
|
2102
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2103
|
+
message: "Error creating tab",
|
|
2104
|
+
code: "CREATE_TAB_ERROR",
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
async closeTab(pageId) {
|
|
2110
|
+
try {
|
|
2111
|
+
await this.browserEmitter?.closeTab(pageId);
|
|
2112
|
+
} catch (error) {
|
|
2113
|
+
this.logger.error("Error closing tab:", error);
|
|
2114
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2115
|
+
message: "Error closing tab",
|
|
2116
|
+
code: "CLOSE_TAB_ERROR",
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
async selectTab(pageId) {
|
|
2122
|
+
try {
|
|
2123
|
+
await this.browserEmitter?.selectTab(pageId);
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
this.logger.error("Error selecting tab:", error);
|
|
2126
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2127
|
+
message: "Error selecting tab",
|
|
2128
|
+
code: "SELECT_TAB_ERROR",
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
async navigateTab({ pageId, url }) {
|
|
2134
|
+
try {
|
|
2135
|
+
if (!pageId || !url) {
|
|
2136
|
+
this.logger.error("navigateTab called without pageId or url", { pageId, url });
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
await this.browserEmitter?.navigateTab(pageId, url);
|
|
2140
|
+
} catch (error) {
|
|
2141
|
+
this.logger.error("Error navigating tab:", error);
|
|
2142
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2143
|
+
message: "Error navigating tab",
|
|
2144
|
+
code: "NAVIGATE_TAB_ERROR",
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
async reloadTab(pageId) {
|
|
2150
|
+
try {
|
|
2151
|
+
await this.browserEmitter?.reloadTab(pageId);
|
|
2152
|
+
} catch (error) {
|
|
2153
|
+
this.logger.error("Error reloading tab:", error);
|
|
2154
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2155
|
+
message: "Error reloading tab",
|
|
2156
|
+
code: "RELOAD_TAB_ERROR",
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
async goBack(pageId) {
|
|
2162
|
+
try {
|
|
2163
|
+
await this.browserEmitter?.goBack(pageId);
|
|
2164
|
+
} catch (error) {
|
|
2165
|
+
this.logger.error("Error navigating back:", error);
|
|
2166
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2167
|
+
message: "Error navigating back",
|
|
2168
|
+
code: "GO_BACK_ERROR",
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
async goForward(pageId) {
|
|
2174
|
+
try {
|
|
2175
|
+
await this.browserEmitter?.goForward(pageId);
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
this.logger.error("Error navigating forward:", error);
|
|
2178
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2179
|
+
message: "Error navigating forward",
|
|
2180
|
+
code: "GO_FORWARD_ERROR",
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
1222
2184
|
}
|