@dev-blinq/cucumber_client 1.0.1457-dev → 1.0.1457-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/scripts/recorder.js +87 -49
- package/bin/assets/scripts/snapshot_capturer.js +10 -17
- package/bin/assets/scripts/unique_locators.js +169 -47
- package/bin/assets/templates/_hooks_template.txt +6 -2
- package/bin/assets/templates/utils_template.txt +16 -16
- package/bin/client/code_cleanup/utils.js +16 -7
- package/bin/client/code_gen/code_inversion.js +115 -0
- package/bin/client/code_gen/duplication_analysis.js +2 -1
- package/bin/client/code_gen/function_signature.js +4 -0
- package/bin/client/code_gen/page_reflection.js +92 -11
- package/bin/client/code_gen/playwright_codeget.js +165 -76
- package/bin/client/cucumber/feature.js +4 -17
- package/bin/client/cucumber/steps_definitions.js +13 -0
- package/bin/client/local_agent.js +1 -0
- package/bin/client/recorderv3/bvt_init.js +320 -0
- package/bin/client/recorderv3/bvt_recorder.js +1312 -63
- package/bin/client/recorderv3/implemented_steps.js +2 -0
- package/bin/client/recorderv3/index.js +3 -293
- package/bin/client/recorderv3/services.js +819 -142
- package/bin/client/recorderv3/step_runner.js +35 -6
- package/bin/client/recorderv3/step_utils.js +175 -95
- package/bin/client/recorderv3/update_feature.js +87 -39
- package/bin/client/recorderv3/wbr_entry.js +61 -0
- package/bin/client/recording.js +1 -0
- package/bin/client/upload-service.js +2 -0
- package/bin/client/utils/app_dir.js +21 -0
- package/bin/client/utils/socket_logger.js +87 -125
- package/bin/index.js +4 -1
- package/package.json +11 -5
- package/bin/client/recorderv3/app_dir.js +0 -23
- package/bin/client/recorderv3/network.js +0 -299
- package/bin/client/recorderv3/scriptTest.js +0 -5
- package/bin/client/recorderv3/ws_server.js +0 -72
|
@@ -1,24 +1,345 @@
|
|
|
1
1
|
// define the jsdoc type for the input
|
|
2
2
|
import { closeContext, initContext, _getDataFile, resetTestData } from "automation_model";
|
|
3
|
-
import { existsSync, readdirSync, readFileSync, rmSync } from "fs";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { rm } from "fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import url from "node:url";
|
|
6
7
|
import { getImplementedSteps, parseRouteFiles } from "./implemented_steps.js";
|
|
7
|
-
import { NamesService } from "./services.js";
|
|
8
|
+
import { NamesService, PublishService } from "./services.js";
|
|
8
9
|
import { BVTStepRunner } from "./step_runner.js";
|
|
9
|
-
import { readFile, writeFile } from "fs/promises";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
11
|
+
import {
|
|
12
|
+
loadStepDefinitions,
|
|
13
|
+
getCommandsForImplementedStep,
|
|
14
|
+
updateStepDefinitions,
|
|
15
|
+
getCodePage,
|
|
16
|
+
getCucumberStep,
|
|
17
|
+
_toRecordingStep,
|
|
18
|
+
toMethodName,
|
|
19
|
+
} from "./step_utils.js";
|
|
12
20
|
import { parseStepTextParameters } from "../cucumber/utils.js";
|
|
13
21
|
import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
|
|
14
22
|
import chokidar from "chokidar";
|
|
15
23
|
import { unEscapeNonPrintables } from "../cucumber/utils.js";
|
|
16
|
-
import { findAvailablePort } from "../utils/index.js";
|
|
17
|
-
import socketLogger from "../utils/socket_logger.js";
|
|
24
|
+
import { findAvailablePort, getRunsServiceBaseURL } from "../utils/index.js";
|
|
25
|
+
import socketLogger, { getErrorMessage } from "../utils/socket_logger.js";
|
|
18
26
|
import { tmpdir } from "os";
|
|
27
|
+
import { faker } from "@faker-js/faker/locale/en_US";
|
|
28
|
+
import { chromium } from "playwright-core";
|
|
29
|
+
import { axiosClient } from "../utils/axiosClient.js";
|
|
30
|
+
import { _generateCodeFromCommand } from "../code_gen/playwright_codeget.js";
|
|
31
|
+
import { Recording } from "../recording.js";
|
|
32
|
+
|
|
19
33
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
20
34
|
|
|
21
35
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
|
|
37
|
+
const clipboardBridgeScript = `
|
|
38
|
+
;(() => {
|
|
39
|
+
if (window.__bvtRecorderClipboardBridgeInitialized) {
|
|
40
|
+
console.log('[ClipboardBridge] Already initialized, skipping');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
window.__bvtRecorderClipboardBridgeInitialized = true;
|
|
44
|
+
console.log('[ClipboardBridge] Initializing clipboard bridge');
|
|
45
|
+
|
|
46
|
+
const emitPayload = (payload, attempt = 0) => {
|
|
47
|
+
const reporter = window.__bvt_reportClipboard;
|
|
48
|
+
if (typeof reporter === "function") {
|
|
49
|
+
try {
|
|
50
|
+
console.log('[ClipboardBridge] Reporting clipboard payload:', payload);
|
|
51
|
+
reporter(payload);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.warn("[ClipboardBridge] Failed to report payload", error);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (attempt < 5) {
|
|
58
|
+
console.log('[ClipboardBridge] Reporter not ready, retrying...', attempt);
|
|
59
|
+
setTimeout(() => emitPayload(payload, attempt + 1), 50 * (attempt + 1));
|
|
60
|
+
} else {
|
|
61
|
+
console.warn('[ClipboardBridge] Reporter never became available');
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const fileToBase64 = (file) => {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
try {
|
|
68
|
+
const reader = new FileReader();
|
|
69
|
+
reader.onload = () => {
|
|
70
|
+
const { result } = reader;
|
|
71
|
+
if (typeof result === "string") {
|
|
72
|
+
const index = result.indexOf("base64,");
|
|
73
|
+
resolve(index !== -1 ? result.substring(index + 7) : result);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (result instanceof ArrayBuffer) {
|
|
77
|
+
const bytes = new Uint8Array(result);
|
|
78
|
+
let binary = "";
|
|
79
|
+
const chunk = 0x8000;
|
|
80
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
81
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
82
|
+
}
|
|
83
|
+
resolve(btoa(binary));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
resolve(null);
|
|
87
|
+
};
|
|
88
|
+
reader.onerror = () => resolve(null);
|
|
89
|
+
reader.readAsDataURL(file);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.warn("[ClipboardBridge] Failed to serialize file", error);
|
|
92
|
+
resolve(null);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const handleClipboardEvent = async (event) => {
|
|
98
|
+
try {
|
|
99
|
+
console.log('[ClipboardBridge] Handling clipboard event:', event.type);
|
|
100
|
+
const payload = { trigger: event.type };
|
|
101
|
+
const clipboardData = event.clipboardData;
|
|
102
|
+
|
|
103
|
+
if (clipboardData) {
|
|
104
|
+
try {
|
|
105
|
+
const text = clipboardData.getData("text/plain");
|
|
106
|
+
if (text) {
|
|
107
|
+
payload.text = text;
|
|
108
|
+
console.log('[ClipboardBridge] Captured text:', text.substring(0, 50));
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.warn("[ClipboardBridge] Could not read text/plain", error);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const html = clipboardData.getData("text/html");
|
|
116
|
+
if (html) {
|
|
117
|
+
payload.html = html;
|
|
118
|
+
console.log('[ClipboardBridge] Captured HTML:', html.substring(0, 50));
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.warn("[ClipboardBridge] Could not read text/html", error);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const files = clipboardData.files;
|
|
125
|
+
if (files && files.length > 0) {
|
|
126
|
+
console.log('[ClipboardBridge] Processing files:', files.length);
|
|
127
|
+
const serialized = [];
|
|
128
|
+
for (const file of files) {
|
|
129
|
+
const data = await fileToBase64(file);
|
|
130
|
+
if (data) {
|
|
131
|
+
serialized.push({
|
|
132
|
+
name: file.name,
|
|
133
|
+
type: file.type,
|
|
134
|
+
lastModified: file.lastModified,
|
|
135
|
+
data,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (serialized.length > 0) {
|
|
140
|
+
payload.files = serialized;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!payload.text) {
|
|
146
|
+
try {
|
|
147
|
+
const selection = window.getSelection?.();
|
|
148
|
+
const selectionText = selection?.toString?.();
|
|
149
|
+
if (selectionText) {
|
|
150
|
+
payload.text = selectionText;
|
|
151
|
+
console.log('[ClipboardBridge] Using selection text:', selectionText.substring(0, 50));
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// Ignore selection access errors.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
emitPayload(payload);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.warn("[ClipboardBridge] Could not process event", error);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// NEW: Function to apply clipboard data to the page
|
|
165
|
+
window.__bvt_applyClipboardData = (payload) => {
|
|
166
|
+
console.log('[ClipboardBridge] Applying clipboard data:', payload);
|
|
167
|
+
|
|
168
|
+
if (!payload) {
|
|
169
|
+
console.warn('[ClipboardBridge] No payload provided');
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
// Create DataTransfer object
|
|
175
|
+
let dataTransfer = null;
|
|
176
|
+
try {
|
|
177
|
+
dataTransfer = new DataTransfer();
|
|
178
|
+
console.log('[ClipboardBridge] DataTransfer created');
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.warn('[ClipboardBridge] Could not create DataTransfer', error);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (dataTransfer) {
|
|
184
|
+
if (payload.text) {
|
|
185
|
+
try {
|
|
186
|
+
dataTransfer.setData("text/plain", payload.text);
|
|
187
|
+
console.log('[ClipboardBridge] Set text/plain:', payload.text.substring(0, 50));
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.warn('[ClipboardBridge] Failed to set text/plain', error);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (payload.html) {
|
|
193
|
+
try {
|
|
194
|
+
dataTransfer.setData("text/html", payload.html);
|
|
195
|
+
console.log('[ClipboardBridge] Set text/html:', payload.html.substring(0, 50));
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.warn('[ClipboardBridge] Failed to set text/html', error);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Get target element
|
|
203
|
+
let target = document.activeElement || document.body;
|
|
204
|
+
console.log('[ClipboardBridge] Target element:', {
|
|
205
|
+
tagName: target.tagName,
|
|
206
|
+
type: target.type,
|
|
207
|
+
isContentEditable: target.isContentEditable,
|
|
208
|
+
id: target.id,
|
|
209
|
+
className: target.className
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Try synthetic paste event first
|
|
213
|
+
let pasteHandled = false;
|
|
214
|
+
if (dataTransfer && target && typeof target.dispatchEvent === "function") {
|
|
215
|
+
try {
|
|
216
|
+
const pasteEvent = new ClipboardEvent("paste", {
|
|
217
|
+
clipboardData: dataTransfer,
|
|
218
|
+
bubbles: true,
|
|
219
|
+
cancelable: true,
|
|
220
|
+
});
|
|
221
|
+
pasteHandled = target.dispatchEvent(pasteEvent);
|
|
222
|
+
console.log('[ClipboardBridge] Paste event dispatched, handled:', pasteHandled);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.warn('[ClipboardBridge] Failed to dispatch paste event', error);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
console.log('[ClipboardBridge] Paste event not handled, trying fallback methods');
|
|
230
|
+
|
|
231
|
+
// Fallback: Try execCommand with HTML first (for contenteditable)
|
|
232
|
+
if (payload.html && target.isContentEditable) {
|
|
233
|
+
console.log('[ClipboardBridge] Trying execCommand insertHTML');
|
|
234
|
+
try {
|
|
235
|
+
const inserted = document.execCommand('insertHTML', false, payload.html);
|
|
236
|
+
if (inserted) {
|
|
237
|
+
console.log('[ClipboardBridge] Successfully inserted HTML via execCommand');
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.warn('[ClipboardBridge] execCommand insertHTML failed', error);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Try Range API for HTML
|
|
245
|
+
console.log('[ClipboardBridge] Trying Range API for HTML');
|
|
246
|
+
try {
|
|
247
|
+
const selection = window.getSelection?.();
|
|
248
|
+
if (selection && selection.rangeCount > 0) {
|
|
249
|
+
const range = selection.getRangeAt(0);
|
|
250
|
+
range.deleteContents();
|
|
251
|
+
const fragment = range.createContextualFragment(payload.html);
|
|
252
|
+
range.insertNode(fragment);
|
|
253
|
+
range.collapse(false);
|
|
254
|
+
console.log('[ClipboardBridge] Successfully inserted HTML via Range API');
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.warn('[ClipboardBridge] Range API HTML insertion failed', error);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Fallback: Try execCommand with text
|
|
263
|
+
if (payload.text) {
|
|
264
|
+
console.log('[ClipboardBridge] Trying execCommand insertText');
|
|
265
|
+
try {
|
|
266
|
+
const inserted = document.execCommand('insertText', false, payload.text);
|
|
267
|
+
if (inserted) {
|
|
268
|
+
console.log('[ClipboardBridge] Successfully inserted text via execCommand');
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.warn('[ClipboardBridge] execCommand insertText failed', error);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Try Range API for text
|
|
276
|
+
if (target.isContentEditable) {
|
|
277
|
+
console.log('[ClipboardBridge] Trying Range API for text');
|
|
278
|
+
try {
|
|
279
|
+
const selection = window.getSelection?.();
|
|
280
|
+
if (selection && selection.rangeCount > 0) {
|
|
281
|
+
const range = selection.getRangeAt(0);
|
|
282
|
+
range.deleteContents();
|
|
283
|
+
range.insertNode(document.createTextNode(payload.text));
|
|
284
|
+
range.collapse(false);
|
|
285
|
+
console.log('[ClipboardBridge] Successfully inserted text via Range API');
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.warn('[ClipboardBridge] Range API text insertion failed', error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Last resort: Direct value assignment for input/textarea
|
|
294
|
+
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
|
|
295
|
+
console.log('[ClipboardBridge] Trying direct value assignment');
|
|
296
|
+
try {
|
|
297
|
+
const start = target.selectionStart ?? target.value.length ?? 0;
|
|
298
|
+
const end = target.selectionEnd ?? target.value.length ?? 0;
|
|
299
|
+
const value = target.value ?? "";
|
|
300
|
+
const text = payload.text;
|
|
301
|
+
target.value = value.slice(0, start) + text + value.slice(end);
|
|
302
|
+
const caret = start + text.length;
|
|
303
|
+
if (typeof target.setSelectionRange === 'function') {
|
|
304
|
+
target.setSelectionRange(caret, caret);
|
|
305
|
+
}
|
|
306
|
+
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
307
|
+
console.log('[ClipboardBridge] Successfully set value directly');
|
|
308
|
+
return true;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.warn('[ClipboardBridge] Direct value assignment failed', error);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
console.warn('[ClipboardBridge] All paste methods failed');
|
|
316
|
+
return false;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.error('[ClipboardBridge] Error applying clipboard data:', error);
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Set up event listeners for copy/cut
|
|
324
|
+
document.addEventListener(
|
|
325
|
+
"copy",
|
|
326
|
+
(event) => {
|
|
327
|
+
void handleClipboardEvent(event);
|
|
328
|
+
},
|
|
329
|
+
true
|
|
330
|
+
);
|
|
331
|
+
document.addEventListener(
|
|
332
|
+
"cut",
|
|
333
|
+
(event) => {
|
|
334
|
+
void handleClipboardEvent(event);
|
|
335
|
+
},
|
|
336
|
+
true
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
console.log('[ClipboardBridge] Clipboard bridge initialized successfully');
|
|
340
|
+
})();
|
|
341
|
+
`;
|
|
342
|
+
|
|
22
343
|
export function getInitScript(config, options) {
|
|
23
344
|
const preScript = `
|
|
24
345
|
window.__bvt_Recorder_config = ${JSON.stringify(config ?? null)};
|
|
@@ -28,7 +349,7 @@ export function getInitScript(config, options) {
|
|
|
28
349
|
path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"),
|
|
29
350
|
"utf8"
|
|
30
351
|
);
|
|
31
|
-
return preScript + recorderScript;
|
|
352
|
+
return preScript + recorderScript + clipboardBridgeScript;
|
|
32
353
|
}
|
|
33
354
|
|
|
34
355
|
async function evaluate(frame, script) {
|
|
@@ -42,7 +363,6 @@ async function evaluate(frame, script) {
|
|
|
42
363
|
}
|
|
43
364
|
|
|
44
365
|
async function findNestedFrameSelector(frame, obj) {
|
|
45
|
-
console.log("Testing new version");
|
|
46
366
|
try {
|
|
47
367
|
const parent = frame.parentFrame();
|
|
48
368
|
if (!parent) return { children: obj };
|
|
@@ -53,8 +373,7 @@ async function findNestedFrameSelector(frame, obj) {
|
|
|
53
373
|
}, frameElement);
|
|
54
374
|
return findNestedFrameSelector(parent, { children: obj, selectors });
|
|
55
375
|
} catch (e) {
|
|
56
|
-
socketLogger.error(`Error in
|
|
57
|
-
console.error(e);
|
|
376
|
+
socketLogger.error(`Error in script evaluation: ${getErrorMessage(e)}`, undefined, "findNestedFrameSelector");
|
|
58
377
|
}
|
|
59
378
|
}
|
|
60
379
|
const transformFillAction = (action, el) => {
|
|
@@ -156,6 +475,17 @@ const transformAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode,
|
|
|
156
475
|
}
|
|
157
476
|
}
|
|
158
477
|
};
|
|
478
|
+
const diffPaths = (currentPath, newPath) => {
|
|
479
|
+
const currentDomain = new URL(currentPath).hostname;
|
|
480
|
+
const newDomain = new URL(newPath).hostname;
|
|
481
|
+
if (currentDomain !== newDomain) {
|
|
482
|
+
return true;
|
|
483
|
+
} else {
|
|
484
|
+
const currentRoute = new URL(currentPath).pathname;
|
|
485
|
+
const newRoute = new URL(newPath).pathname;
|
|
486
|
+
return currentRoute !== newRoute;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
159
489
|
/**
|
|
160
490
|
* @typedef {Object} BVTRecorderInput
|
|
161
491
|
* @property {string} envName
|
|
@@ -185,12 +515,16 @@ export class BVTRecorder {
|
|
|
185
515
|
projectDir: this.projectDir,
|
|
186
516
|
logger: this.logger,
|
|
187
517
|
});
|
|
518
|
+
this.workspaceService = new PublishService(this.TOKEN);
|
|
188
519
|
this.pageSet = new Set();
|
|
189
520
|
this.lastKnownUrlPath = "";
|
|
190
|
-
this.world = { attach: () => {} };
|
|
521
|
+
this.world = { attach: () => { } };
|
|
191
522
|
this.shouldTakeScreenshot = true;
|
|
192
523
|
this.watcher = null;
|
|
193
524
|
this.networkEventsFolder = path.join(tmpdir(), "blinq_network_events");
|
|
525
|
+
this.tempProjectFolder = `${tmpdir()}/bvt_temp_project_${Math.floor(Math.random() * 1000000)}`;
|
|
526
|
+
this.tempSnapshotsFolder = path.join(this.tempProjectFolder, "data/snapshots");
|
|
527
|
+
|
|
194
528
|
if (existsSync(this.networkEventsFolder)) {
|
|
195
529
|
rmSync(this.networkEventsFolder, { recursive: true, force: true });
|
|
196
530
|
}
|
|
@@ -208,6 +542,12 @@ export class BVTRecorder {
|
|
|
208
542
|
cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
|
|
209
543
|
cmdExecutionError: "BVTRecorder.cmdExecutionError",
|
|
210
544
|
interceptResults: "BVTRecorder.interceptResults",
|
|
545
|
+
onDebugURLChange: "BVTRecorder.onDebugURLChange",
|
|
546
|
+
updateCommand: "BVTRecorder.updateCommand",
|
|
547
|
+
browserStateSync: "BrowserService.stateSync",
|
|
548
|
+
browserStateError: "BrowserService.stateError",
|
|
549
|
+
clipboardPush: "BrowserService.clipboardPush",
|
|
550
|
+
clipboardError: "BrowserService.clipboardError",
|
|
211
551
|
};
|
|
212
552
|
bindings = {
|
|
213
553
|
__bvt_recordCommand: async ({ frame, page, context }, event) => {
|
|
@@ -237,6 +577,25 @@ export class BVTRecorder {
|
|
|
237
577
|
__bvt_getObject: (_src, obj) => {
|
|
238
578
|
this.processObject(obj);
|
|
239
579
|
},
|
|
580
|
+
__bvt_reportClipboard: async ({ page }, payload) => {
|
|
581
|
+
try {
|
|
582
|
+
if (!payload) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
586
|
+
if (activePage && activePage !== page) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const pageUrl = typeof page?.url === "function" ? page.url() : null;
|
|
590
|
+
this.sendEvent(this.events.clipboardPush, {
|
|
591
|
+
data: payload,
|
|
592
|
+
trigger: payload?.trigger ?? "copy",
|
|
593
|
+
pageUrl,
|
|
594
|
+
});
|
|
595
|
+
} catch (error) {
|
|
596
|
+
this.logger.error("Error forwarding clipboard payload from page", error);
|
|
597
|
+
}
|
|
598
|
+
},
|
|
240
599
|
};
|
|
241
600
|
|
|
242
601
|
getSnapshot = async (attr) => {
|
|
@@ -294,11 +653,15 @@ export class BVTRecorder {
|
|
|
294
653
|
}
|
|
295
654
|
|
|
296
655
|
async _initBrowser({ url }) {
|
|
297
|
-
|
|
298
|
-
|
|
656
|
+
if (process.env.CDP_LISTEN_PORT === undefined) {
|
|
657
|
+
this.#remoteDebuggerPort = await findAvailablePort();
|
|
658
|
+
process.env.CDP_LISTEN_PORT = this.#remoteDebuggerPort;
|
|
659
|
+
} else {
|
|
660
|
+
this.#remoteDebuggerPort = process.env.CDP_LISTEN_PORT;
|
|
661
|
+
}
|
|
299
662
|
|
|
300
663
|
// this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
|
|
301
|
-
this.world = { attach: () => {} };
|
|
664
|
+
this.world = { attach: () => { } };
|
|
302
665
|
|
|
303
666
|
const ai_config_file = path.join(this.projectDir, "ai_config.json");
|
|
304
667
|
let ai_config = {};
|
|
@@ -318,7 +681,8 @@ export class BVTRecorder {
|
|
|
318
681
|
],
|
|
319
682
|
};
|
|
320
683
|
|
|
321
|
-
const
|
|
684
|
+
const scenario = { pickle: this.scenarioDoc };
|
|
685
|
+
const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName, scenario);
|
|
322
686
|
this.bvtContext = bvtContext;
|
|
323
687
|
this.stepRunner = new BVTStepRunner({
|
|
324
688
|
projectDir: this.projectDir,
|
|
@@ -344,8 +708,7 @@ export class BVTRecorder {
|
|
|
344
708
|
},
|
|
345
709
|
bvtContext: this.bvtContext,
|
|
346
710
|
});
|
|
347
|
-
|
|
348
|
-
this.context = context;
|
|
711
|
+
this.context = bvtContext.playContext;
|
|
349
712
|
this.web = bvtContext.stable || bvtContext.web;
|
|
350
713
|
this.web.tryAllStrategies = true;
|
|
351
714
|
this.page = bvtContext.page;
|
|
@@ -359,9 +722,19 @@ export class BVTRecorder {
|
|
|
359
722
|
await this.context.exposeBinding(name, handler);
|
|
360
723
|
}
|
|
361
724
|
this._watchTestData();
|
|
362
|
-
this.web.onRestoreSaveState = (url) => {
|
|
363
|
-
this._initBrowser({ url });
|
|
725
|
+
this.web.onRestoreSaveState = async (url) => {
|
|
726
|
+
await this._initBrowser({ url });
|
|
727
|
+
this._addPagelisteners(this.context);
|
|
728
|
+
this._addFrameNavigateListener(this.page);
|
|
364
729
|
};
|
|
730
|
+
|
|
731
|
+
// create a second browser for locator generation
|
|
732
|
+
this.backgroundBrowser = await chromium.launch({
|
|
733
|
+
headless: true,
|
|
734
|
+
});
|
|
735
|
+
this.backgroundContext = await this.backgroundBrowser.newContext({});
|
|
736
|
+
await this.backgroundContext.addInitScript({ content: this.getInitScripts(this.config) });
|
|
737
|
+
await this.backgroundContext.newPage();
|
|
365
738
|
}
|
|
366
739
|
async onClosePopup() {
|
|
367
740
|
// console.log("close popups");
|
|
@@ -403,13 +776,14 @@ export class BVTRecorder {
|
|
|
403
776
|
|
|
404
777
|
await this.page.goto(url, {
|
|
405
778
|
waitUntil: "domcontentloaded",
|
|
779
|
+
timeout: this.config.page_timeout ?? 60_000,
|
|
406
780
|
});
|
|
407
781
|
// add listener for frame navigation on current tab
|
|
408
782
|
this._addFrameNavigateListener(this.page);
|
|
409
783
|
|
|
410
784
|
// eval init script on current tab
|
|
411
785
|
// await this._initPage(this.page);
|
|
412
|
-
this.#currentURL =
|
|
786
|
+
this.#currentURL = url;
|
|
413
787
|
|
|
414
788
|
await this.page.dispatchEvent("html", "scroll");
|
|
415
789
|
await delay(1000);
|
|
@@ -451,14 +825,15 @@ export class BVTRecorder {
|
|
|
451
825
|
element: { inputID: "frame" },
|
|
452
826
|
});
|
|
453
827
|
|
|
454
|
-
const
|
|
828
|
+
const newUrl = frame.url();
|
|
829
|
+
const newPath = new URL(newUrl).pathname;
|
|
455
830
|
const newTitle = await frame.title();
|
|
456
|
-
|
|
831
|
+
const changed = diffPaths(this.#currentURL, newUrl);
|
|
832
|
+
|
|
833
|
+
if (changed) {
|
|
457
834
|
this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
|
|
458
|
-
this.#currentURL =
|
|
835
|
+
this.#currentURL = newUrl;
|
|
459
836
|
}
|
|
460
|
-
// await this._setRecordingMode(frame);
|
|
461
|
-
// await this._initPage(page);
|
|
462
837
|
} catch (error) {
|
|
463
838
|
this.logger.error("Error in frame navigate event");
|
|
464
839
|
this.logger.error(error);
|
|
@@ -623,7 +998,6 @@ export class BVTRecorder {
|
|
|
623
998
|
try {
|
|
624
999
|
if (page.isClosed()) return;
|
|
625
1000
|
this.pageSet.add(page);
|
|
626
|
-
|
|
627
1001
|
await page.waitForLoadState("domcontentloaded");
|
|
628
1002
|
|
|
629
1003
|
// add listener for frame navigation on new tab
|
|
@@ -688,6 +1062,52 @@ export class BVTRecorder {
|
|
|
688
1062
|
console.error("Error in saving screenshot: ", error);
|
|
689
1063
|
}
|
|
690
1064
|
}
|
|
1065
|
+
async generateLocators(event) {
|
|
1066
|
+
const snapshotDetails = event.snapshotDetails;
|
|
1067
|
+
if (!snapshotDetails) {
|
|
1068
|
+
throw new Error("No snapshot details found");
|
|
1069
|
+
}
|
|
1070
|
+
const mode = event.mode;
|
|
1071
|
+
const inputID = event.element.inputID;
|
|
1072
|
+
|
|
1073
|
+
const { id, contextId, doc } = snapshotDetails;
|
|
1074
|
+
// const selector = `[data-blinq-id="${id}"]`;
|
|
1075
|
+
const newPage = await this.backgroundContext.newPage();
|
|
1076
|
+
await newPage.setContent(doc, { waitUntil: "domcontentloaded" });
|
|
1077
|
+
const locatorsObj = await newPage.evaluate(
|
|
1078
|
+
([id, contextId, mode]) => {
|
|
1079
|
+
const recorder = window.__bvt_Recorder;
|
|
1080
|
+
const contextElement = document.querySelector(`[data-blinq-context-id="${contextId}"]`);
|
|
1081
|
+
const el = document.querySelector(`[data-blinq-id="${id}"]`);
|
|
1082
|
+
if (contextElement) {
|
|
1083
|
+
const result = recorder.locatorGenerator.toContextLocators(el, contextElement);
|
|
1084
|
+
return result;
|
|
1085
|
+
}
|
|
1086
|
+
const isRecordingText = mode === "recordingText";
|
|
1087
|
+
return recorder.locatorGenerator.getElementLocators(el, {
|
|
1088
|
+
excludeText: isRecordingText,
|
|
1089
|
+
});
|
|
1090
|
+
},
|
|
1091
|
+
[id, contextId, mode]
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
// console.log(`Generated locators: for ${inputID}: `, JSON.stringify(locatorsObj));
|
|
1095
|
+
await newPage.close();
|
|
1096
|
+
if (event.nestFrmLoc?.children) {
|
|
1097
|
+
locatorsObj.nestFrmLoc = event.nestFrmLoc.children;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
this.sendEvent(this.events.updateCommand, {
|
|
1101
|
+
locators: {
|
|
1102
|
+
locators: locatorsObj.locators,
|
|
1103
|
+
nestFrmLoc: locatorsObj.nestFrmLoc,
|
|
1104
|
+
iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
1105
|
+
},
|
|
1106
|
+
allStrategyLocators: locatorsObj.allStrategyLocators,
|
|
1107
|
+
inputID,
|
|
1108
|
+
});
|
|
1109
|
+
// const
|
|
1110
|
+
}
|
|
691
1111
|
async onAction(event) {
|
|
692
1112
|
this._updateUrlPath();
|
|
693
1113
|
// const locators = this.overlayLocators(event);
|
|
@@ -701,25 +1121,41 @@ export class BVTRecorder {
|
|
|
701
1121
|
event.mode === "recordingHover",
|
|
702
1122
|
event.mode === "multiInspecting"
|
|
703
1123
|
),
|
|
704
|
-
locators: {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
},
|
|
708
|
-
allStrategyLocators: event.allStrategyLocators,
|
|
1124
|
+
// locators: {
|
|
1125
|
+
// locators: event.locators,
|
|
1126
|
+
// iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
1127
|
+
// },
|
|
1128
|
+
// allStrategyLocators: event.allStrategyLocators,
|
|
709
1129
|
url: event.frame.url,
|
|
710
1130
|
title: event.frame.title,
|
|
711
1131
|
extract: {},
|
|
712
1132
|
lastKnownUrlPath: this.lastKnownUrlPath,
|
|
713
1133
|
};
|
|
714
|
-
if (event.nestFrmLoc?.children) {
|
|
715
|
-
|
|
716
|
-
}
|
|
1134
|
+
// if (event.nestFrmLoc?.children) {
|
|
1135
|
+
// cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
|
|
1136
|
+
// }
|
|
717
1137
|
// this.logger.info({ event });
|
|
718
1138
|
if (this.shouldTakeScreenshot) {
|
|
719
1139
|
await this.storeScreenshot(event);
|
|
720
1140
|
}
|
|
721
|
-
this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
722
|
-
this._updateUrlPath();
|
|
1141
|
+
// this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
1142
|
+
// this._updateUrlPath();
|
|
1143
|
+
if (event.locators) {
|
|
1144
|
+
Object.assign(cmdEvent, {
|
|
1145
|
+
locators: {
|
|
1146
|
+
locators: event.locators,
|
|
1147
|
+
iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
1148
|
+
nestFrmLoc: event.nestFrmLoc?.children,
|
|
1149
|
+
},
|
|
1150
|
+
allStrategyLocators: event.allStrategyLocators,
|
|
1151
|
+
})
|
|
1152
|
+
this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
1153
|
+
this._updateUrlPath();
|
|
1154
|
+
} else {
|
|
1155
|
+
this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
1156
|
+
this._updateUrlPath();
|
|
1157
|
+
await this.generateLocators(event);
|
|
1158
|
+
}
|
|
723
1159
|
}
|
|
724
1160
|
_updateUrlPath() {
|
|
725
1161
|
try {
|
|
@@ -735,13 +1171,12 @@ export class BVTRecorder {
|
|
|
735
1171
|
}
|
|
736
1172
|
async closeBrowser() {
|
|
737
1173
|
delete process.env.TEMP_RUN;
|
|
738
|
-
await this.watcher.close().then(() => {});
|
|
1174
|
+
await this.watcher.close().then(() => { });
|
|
739
1175
|
this.watcher = null;
|
|
740
1176
|
this.previousIndex = null;
|
|
741
1177
|
this.previousHistoryLength = null;
|
|
742
1178
|
this.previousUrl = null;
|
|
743
1179
|
this.previousEntries = null;
|
|
744
|
-
|
|
745
1180
|
await closeContext();
|
|
746
1181
|
this.pageSet.clear();
|
|
747
1182
|
}
|
|
@@ -764,25 +1199,26 @@ export class BVTRecorder {
|
|
|
764
1199
|
for (let i = 0; i < 3; i++) {
|
|
765
1200
|
result = 0;
|
|
766
1201
|
try {
|
|
767
|
-
for (const page of this.context.pages()) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1202
|
+
// for (const page of this.context.pages()) {
|
|
1203
|
+
const page = this.web.page;
|
|
1204
|
+
for (const frame of page.frames()) {
|
|
1205
|
+
try {
|
|
1206
|
+
//scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params: Params)
|
|
1207
|
+
const frameResult = await this.web._locateElementByText(
|
|
1208
|
+
frame,
|
|
1209
|
+
searchString,
|
|
1210
|
+
tag,
|
|
1211
|
+
regex,
|
|
1212
|
+
partial,
|
|
1213
|
+
ignoreCase,
|
|
1214
|
+
{}
|
|
1215
|
+
);
|
|
1216
|
+
result += frameResult.elementCount;
|
|
1217
|
+
} catch (e) {
|
|
1218
|
+
console.log(e);
|
|
784
1219
|
}
|
|
785
1220
|
}
|
|
1221
|
+
// }
|
|
786
1222
|
|
|
787
1223
|
return result;
|
|
788
1224
|
} catch (e) {
|
|
@@ -833,15 +1269,21 @@ export class BVTRecorder {
|
|
|
833
1269
|
}, 100);
|
|
834
1270
|
this.timerId = timerId;
|
|
835
1271
|
}
|
|
836
|
-
async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
|
|
1272
|
+
async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork, AICode }, options) {
|
|
837
1273
|
const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
|
|
1274
|
+
|
|
1275
|
+
const env = path.basename(this.envName, ".json");
|
|
838
1276
|
const _env = {
|
|
839
1277
|
TOKEN: this.TOKEN,
|
|
840
1278
|
TEMP_RUN: true,
|
|
841
1279
|
REPORT_FOLDER: this.bvtContext.reportFolder,
|
|
842
1280
|
BLINQ_ENV: this.envName,
|
|
843
1281
|
DEBUG: "blinq:route",
|
|
1282
|
+
// BVT_TEMP_SNAPSHOTS_FOLDER: step.isImplemented ? path.join(this.tempSnapshotsFolder, env) : undefined,
|
|
844
1283
|
};
|
|
1284
|
+
if (!step.isImplemented) {
|
|
1285
|
+
_env.BVT_TEMP_SNAPSHOTS_FOLDER = path.join(this.tempSnapshotsFolder, env);
|
|
1286
|
+
}
|
|
845
1287
|
|
|
846
1288
|
this.bvtContext.navigate = true;
|
|
847
1289
|
this.bvtContext.loadedRoutes = null;
|
|
@@ -861,6 +1303,7 @@ export class BVTRecorder {
|
|
|
861
1303
|
await this.setMode("running");
|
|
862
1304
|
|
|
863
1305
|
try {
|
|
1306
|
+
step.text = step.text.trim();
|
|
864
1307
|
const { result, info } = await this.stepRunner.runStep(
|
|
865
1308
|
{
|
|
866
1309
|
step,
|
|
@@ -868,6 +1311,7 @@ export class BVTRecorder {
|
|
|
868
1311
|
envPath: this.envName,
|
|
869
1312
|
tags,
|
|
870
1313
|
config: this.config,
|
|
1314
|
+
AICode,
|
|
871
1315
|
},
|
|
872
1316
|
this.bvtContext,
|
|
873
1317
|
{
|
|
@@ -887,10 +1331,23 @@ export class BVTRecorder {
|
|
|
887
1331
|
this.bvtContext.navigate = false;
|
|
888
1332
|
}
|
|
889
1333
|
}
|
|
890
|
-
async saveScenario({ scenario, featureName, override, isSingleStep }) {
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1334
|
+
async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env, AICode }) {
|
|
1335
|
+
const res = await this.workspaceService.saveScenario({
|
|
1336
|
+
scenario,
|
|
1337
|
+
featureName,
|
|
1338
|
+
override,
|
|
1339
|
+
isSingleStep,
|
|
1340
|
+
branch,
|
|
1341
|
+
isEditing,
|
|
1342
|
+
projectId: path.basename(this.projectDir),
|
|
1343
|
+
env: env ?? this.envName,
|
|
1344
|
+
AICode,
|
|
1345
|
+
});
|
|
1346
|
+
if (res.success) {
|
|
1347
|
+
await this.cleanup({ tags: scenario.tags });
|
|
1348
|
+
} else {
|
|
1349
|
+
throw new Error(res.message || "Error saving scenario");
|
|
1350
|
+
}
|
|
894
1351
|
}
|
|
895
1352
|
async getImplementedSteps() {
|
|
896
1353
|
const stepsAndScenarios = await getImplementedSteps(this.projectDir);
|
|
@@ -1055,9 +1512,11 @@ export class BVTRecorder {
|
|
|
1055
1512
|
const featureFilePath = path.join(this.projectDir, "features", featureName);
|
|
1056
1513
|
const gherkinDoc = this.parseFeatureFile(featureFilePath);
|
|
1057
1514
|
const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
|
|
1515
|
+
this.scenarioDoc = scenario;
|
|
1058
1516
|
|
|
1059
1517
|
const steps = [];
|
|
1060
1518
|
const parameters = [];
|
|
1519
|
+
const datasets = [];
|
|
1061
1520
|
if (scenario.examples && scenario.examples.length > 0) {
|
|
1062
1521
|
const example = scenario.examples[0];
|
|
1063
1522
|
example?.tableHeader?.cells.forEach((cell, index) => {
|
|
@@ -1065,7 +1524,26 @@ export class BVTRecorder {
|
|
|
1065
1524
|
key: cell.value,
|
|
1066
1525
|
value: unEscapeNonPrintables(example.tableBody[0].cells[index].value),
|
|
1067
1526
|
});
|
|
1527
|
+
// datasets.push({
|
|
1528
|
+
// data: example.tableBody[]
|
|
1529
|
+
// })
|
|
1068
1530
|
});
|
|
1531
|
+
|
|
1532
|
+
for (let i = 0; i < example.tableBody.length; i++) {
|
|
1533
|
+
const row = example.tableBody[i];
|
|
1534
|
+
// for (const row of example.tableBody) {
|
|
1535
|
+
const paramters = [];
|
|
1536
|
+
row.cells.forEach((cell, index) => {
|
|
1537
|
+
paramters.push({
|
|
1538
|
+
key: example.tableHeader.cells[index].value,
|
|
1539
|
+
value: unEscapeNonPrintables(cell.value),
|
|
1540
|
+
});
|
|
1541
|
+
});
|
|
1542
|
+
datasets.push({
|
|
1543
|
+
data: paramters,
|
|
1544
|
+
datasetId: i,
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1069
1547
|
}
|
|
1070
1548
|
|
|
1071
1549
|
for (const step of scenario.steps) {
|
|
@@ -1086,6 +1564,7 @@ export class BVTRecorder {
|
|
|
1086
1564
|
tags: scenario.tags.map((tag) => tag.name),
|
|
1087
1565
|
steps,
|
|
1088
1566
|
parameters,
|
|
1567
|
+
datasets,
|
|
1089
1568
|
};
|
|
1090
1569
|
}
|
|
1091
1570
|
async findRelatedTextInAllFrames({ searchString, climb, contextText, params }) {
|
|
@@ -1118,6 +1597,271 @@ export class BVTRecorder {
|
|
|
1118
1597
|
}
|
|
1119
1598
|
return result;
|
|
1120
1599
|
}
|
|
1600
|
+
async setStepCodeByScenario({
|
|
1601
|
+
function_name,
|
|
1602
|
+
mjs_file_content,
|
|
1603
|
+
user_request,
|
|
1604
|
+
selectedTarget,
|
|
1605
|
+
page_context,
|
|
1606
|
+
AIMemory,
|
|
1607
|
+
steps_context,
|
|
1608
|
+
}) {
|
|
1609
|
+
const runsURL = getRunsServiceBaseURL();
|
|
1610
|
+
const url = `${runsURL}/process-user-request/generate-code-with-context`;
|
|
1611
|
+
try {
|
|
1612
|
+
const result = await axiosClient({
|
|
1613
|
+
url,
|
|
1614
|
+
method: "POST",
|
|
1615
|
+
data: {
|
|
1616
|
+
function_name,
|
|
1617
|
+
mjs_file_content,
|
|
1618
|
+
user_request,
|
|
1619
|
+
selectedTarget,
|
|
1620
|
+
page_context,
|
|
1621
|
+
AIMemory,
|
|
1622
|
+
steps_context,
|
|
1623
|
+
},
|
|
1624
|
+
headers: {
|
|
1625
|
+
Authorization: `Bearer ${this.TOKEN}`,
|
|
1626
|
+
"X-Source": "recorder",
|
|
1627
|
+
},
|
|
1628
|
+
});
|
|
1629
|
+
if (result.status !== 200) {
|
|
1630
|
+
return { success: false, message: "Error while fetching code changes" };
|
|
1631
|
+
}
|
|
1632
|
+
return { success: true, data: result.data };
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
// @ts-ignore
|
|
1635
|
+
const reason = error?.response?.data?.error || "";
|
|
1636
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1637
|
+
throw new Error(`Failed to fetch code changes: ${errorMessage} \n ${reason}`);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
async getStepCodeByScenario({ featureName, scenarioName, projectId, branch }) {
|
|
1642
|
+
try {
|
|
1643
|
+
const runsURL = getRunsServiceBaseURL();
|
|
1644
|
+
const ssoURL = runsURL.replace("/runs", "/auth");
|
|
1645
|
+
const privateRepoURL = `${ssoURL}/isRepoPrivate?project_id=${projectId}`;
|
|
1646
|
+
|
|
1647
|
+
const isPrivateRepoReq = await axiosClient({
|
|
1648
|
+
url: privateRepoURL,
|
|
1649
|
+
method: "GET",
|
|
1650
|
+
headers: {
|
|
1651
|
+
Authorization: `Bearer ${this.TOKEN}`,
|
|
1652
|
+
"X-Source": "recorder",
|
|
1653
|
+
},
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
if (isPrivateRepoReq.status !== 200) {
|
|
1657
|
+
return { success: false, message: "Error while checking repo privacy" };
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
const isPrivateRepo = isPrivateRepoReq.data.isPrivate ? isPrivateRepoReq.data.isPrivate : false;
|
|
1661
|
+
|
|
1662
|
+
const workspaceURL = runsURL.replace("/runs", "/workspace");
|
|
1663
|
+
const url = `${workspaceURL}/get-step-code-by-scenario`;
|
|
1664
|
+
|
|
1665
|
+
const result = await axiosClient({
|
|
1666
|
+
url,
|
|
1667
|
+
method: "POST",
|
|
1668
|
+
data: {
|
|
1669
|
+
scenarioName,
|
|
1670
|
+
featureName,
|
|
1671
|
+
projectId,
|
|
1672
|
+
isPrivateRepo,
|
|
1673
|
+
branch,
|
|
1674
|
+
},
|
|
1675
|
+
headers: {
|
|
1676
|
+
Authorization: `Bearer ${this.TOKEN}`,
|
|
1677
|
+
"X-Source": "recorder",
|
|
1678
|
+
},
|
|
1679
|
+
});
|
|
1680
|
+
if (result.status !== 200) {
|
|
1681
|
+
return { success: false, message: "Error while getting step code" };
|
|
1682
|
+
}
|
|
1683
|
+
return { success: true, data: result.data.stepInfo };
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
const reason = error?.response?.data?.error || "";
|
|
1686
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1687
|
+
throw new Error(`Failed to get step code: ${errorMessage} \n ${reason}`);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
async getContext() {
|
|
1691
|
+
await this.page.waitForLoadState("domcontentloaded");
|
|
1692
|
+
await this.page.waitForSelector("body");
|
|
1693
|
+
await this.page.waitForTimeout(500);
|
|
1694
|
+
return await this.page.evaluate(() => {
|
|
1695
|
+
return document.documentElement.outerHTML;
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
async deleteCommandFromStepCode({ scenario, AICode, command }) {
|
|
1699
|
+
if (!AICode || AICode.length === 0) {
|
|
1700
|
+
console.log("No AI code available to delete.");
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
|
|
1705
|
+
const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
|
|
1706
|
+
process.env.tempFeaturesFolderPath = __temp_features_FolderName;
|
|
1707
|
+
process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
|
|
1708
|
+
|
|
1709
|
+
try {
|
|
1710
|
+
await this.stepRunner.copyCodetoTempFolder({ tempFolderPath, AICode });
|
|
1711
|
+
await this.stepRunner.writeWrapperCode(tempFolderPath);
|
|
1712
|
+
const codeView = AICode.find((f) => f.stepName === scenario.step.text);
|
|
1713
|
+
|
|
1714
|
+
if (!codeView) {
|
|
1715
|
+
throw new Error("Step code not found for step: " + scenario.step.text);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
const functionName = codeView.functionName;
|
|
1719
|
+
const mjsPath = path
|
|
1720
|
+
.normalize(codeView.mjsFile)
|
|
1721
|
+
.split(path.sep)
|
|
1722
|
+
.filter((part) => part !== "features")
|
|
1723
|
+
.join(path.sep);
|
|
1724
|
+
const codePath = path.join(tempFolderPath, mjsPath);
|
|
1725
|
+
|
|
1726
|
+
if (!existsSync(codePath)) {
|
|
1727
|
+
throw new Error("Step code file not found: " + codePath);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
const codePage = getCodePage(codePath);
|
|
1731
|
+
|
|
1732
|
+
const elements = codePage.getVariableDeclarationAsObject("elements");
|
|
1733
|
+
|
|
1734
|
+
const cucumberStep = getCucumberStep({ step: scenario.step });
|
|
1735
|
+
cucumberStep.text = scenario.step.text;
|
|
1736
|
+
const stepCommands = scenario.step.commands;
|
|
1737
|
+
const cmd = _toRecordingStep(command, scenario.step.name);
|
|
1738
|
+
|
|
1739
|
+
const recording = new Recording();
|
|
1740
|
+
recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
|
|
1741
|
+
const step = { ...recording.steps[0], ...cmd };
|
|
1742
|
+
const result = _generateCodeFromCommand(step, elements, {});
|
|
1743
|
+
|
|
1744
|
+
codePage._removeCommands(functionName, result.codeLines);
|
|
1745
|
+
codePage.removeUnusedElements();
|
|
1746
|
+
codePage.save();
|
|
1747
|
+
|
|
1748
|
+
await rm(tempFolderPath, { recursive: true, force: true });
|
|
1749
|
+
|
|
1750
|
+
return { code: codePage.fileContent, mjsFile: codeView.mjsFile };
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
await rm(tempFolderPath, { recursive: true, force: true });
|
|
1753
|
+
throw error;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
async addCommandToStepCode({ scenario, AICode }) {
|
|
1757
|
+
if (!AICode || AICode.length === 0) {
|
|
1758
|
+
console.log("No AI code available to add.");
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
|
|
1763
|
+
const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
|
|
1764
|
+
process.env.tempFeaturesFolderPath = __temp_features_FolderName;
|
|
1765
|
+
process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
|
|
1766
|
+
|
|
1767
|
+
try {
|
|
1768
|
+
await this.stepRunner.copyCodetoTempFolder({ tempFolderPath, AICode });
|
|
1769
|
+
await this.stepRunner.writeWrapperCode(tempFolderPath);
|
|
1770
|
+
|
|
1771
|
+
let codeView = AICode.find((f) => f.stepName === scenario.step.text);
|
|
1772
|
+
|
|
1773
|
+
if (codeView) {
|
|
1774
|
+
scenario.step.commands = [scenario.step.commands.pop()];
|
|
1775
|
+
const functionName = codeView.functionName;
|
|
1776
|
+
const mjsPath = path
|
|
1777
|
+
.normalize(codeView.mjsFile)
|
|
1778
|
+
.split(path.sep)
|
|
1779
|
+
.filter((part) => part !== "features")
|
|
1780
|
+
.join(path.sep);
|
|
1781
|
+
const codePath = path.join(tempFolderPath, mjsPath);
|
|
1782
|
+
|
|
1783
|
+
if (!existsSync(codePath)) {
|
|
1784
|
+
throw new Error("Step code file not found: " + codePath);
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
const codePage = getCodePage(codePath);
|
|
1788
|
+
const elements = codePage.getVariableDeclarationAsObject("elements");
|
|
1789
|
+
|
|
1790
|
+
const cucumberStep = getCucumberStep({ step: scenario.step });
|
|
1791
|
+
cucumberStep.text = scenario.step.text;
|
|
1792
|
+
const stepCommands = scenario.step.commands;
|
|
1793
|
+
const cmd = _toRecordingStep(scenario.step.commands[0], scenario.step.name);
|
|
1794
|
+
|
|
1795
|
+
const recording = new Recording();
|
|
1796
|
+
recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
|
|
1797
|
+
const step = { ...recording.steps[0], ...cmd };
|
|
1798
|
+
|
|
1799
|
+
const result = _generateCodeFromCommand(step, elements, {});
|
|
1800
|
+
codePage.insertElements(result.elements);
|
|
1801
|
+
|
|
1802
|
+
codePage._injectOneCommand(functionName, result.codeLines.join("\n"));
|
|
1803
|
+
codePage.save();
|
|
1804
|
+
|
|
1805
|
+
await rm(tempFolderPath, { recursive: true, force: true });
|
|
1806
|
+
|
|
1807
|
+
return { code: codePage.fileContent, newStep: false, mjsFile: codeView.mjsFile };
|
|
1808
|
+
}
|
|
1809
|
+
console.log("Step code not found for step: ", scenario.step.text);
|
|
1810
|
+
|
|
1811
|
+
codeView = AICode[0];
|
|
1812
|
+
const functionName = toMethodName(scenario.step.text);
|
|
1813
|
+
const codeLines = [];
|
|
1814
|
+
const mjsPath = path
|
|
1815
|
+
.normalize(codeView.mjsFile)
|
|
1816
|
+
.split(path.sep)
|
|
1817
|
+
.filter((part) => part !== "features")
|
|
1818
|
+
.join(path.sep);
|
|
1819
|
+
const codePath = path.join(tempFolderPath, mjsPath);
|
|
1820
|
+
|
|
1821
|
+
if (!existsSync(codePath)) {
|
|
1822
|
+
throw new Error("Step code file not found: " + codePath);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
const codePage = getCodePage(codePath);
|
|
1826
|
+
const elements = codePage.getVariableDeclarationAsObject("elements");
|
|
1827
|
+
let newElements = { ...elements };
|
|
1828
|
+
|
|
1829
|
+
const cucumberStep = getCucumberStep({ step: scenario.step });
|
|
1830
|
+
cucumberStep.text = scenario.step.text;
|
|
1831
|
+
const stepCommands = scenario.step.commands;
|
|
1832
|
+
stepCommands.forEach((command) => {
|
|
1833
|
+
const cmd = _toRecordingStep(command, scenario.step.name);
|
|
1834
|
+
|
|
1835
|
+
const recording = new Recording();
|
|
1836
|
+
recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
|
|
1837
|
+
const step = { ...recording.steps[0], ...cmd };
|
|
1838
|
+
const result = _generateCodeFromCommand(step, elements, {});
|
|
1839
|
+
newElements = { ...result.elements };
|
|
1840
|
+
codeLines.push(...result.codeLines);
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
codePage.insertElements(newElements);
|
|
1844
|
+
codePage.addInfraCommand(
|
|
1845
|
+
functionName,
|
|
1846
|
+
cucumberStep.text,
|
|
1847
|
+
cucumberStep.getVariablesList(),
|
|
1848
|
+
codeLines,
|
|
1849
|
+
false,
|
|
1850
|
+
"recorder"
|
|
1851
|
+
);
|
|
1852
|
+
|
|
1853
|
+
const keyword = (cucumberStep.keywordAlias ?? cucumberStep.keyword).trim();
|
|
1854
|
+
codePage.addCucumberStep(keyword, cucumberStep.getTemplate(), functionName, stepCommands.length);
|
|
1855
|
+
codePage.save();
|
|
1856
|
+
|
|
1857
|
+
await rm(tempFolderPath, { recursive: true, force: true });
|
|
1858
|
+
|
|
1859
|
+
return { code: codePage.fileContent, newStep: true, functionName, mjsFile: codeView.mjsFile };
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
await rm(tempFolderPath, { recursive: true, force: true });
|
|
1862
|
+
throw error;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1121
1865
|
async cleanup({ tags }) {
|
|
1122
1866
|
const noopStep = {
|
|
1123
1867
|
text: "Noop",
|
|
@@ -1235,4 +1979,509 @@ export class BVTRecorder {
|
|
|
1235
1979
|
this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
|
|
1236
1980
|
}
|
|
1237
1981
|
}
|
|
1982
|
+
|
|
1983
|
+
async fakeParams(params) {
|
|
1984
|
+
const newFakeParams = {};
|
|
1985
|
+
Object.keys(params).forEach((key) => {
|
|
1986
|
+
if (!params[key].startsWith("{{") || !params[key].endsWith("}}")) {
|
|
1987
|
+
newFakeParams[key] = params[key];
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
try {
|
|
1992
|
+
const value = params[key].substring(2, params[key].length - 2).trim();
|
|
1993
|
+
const faking = value.split("(")[0].split(".");
|
|
1994
|
+
let argument = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
|
|
1995
|
+
argument = isNaN(Number(argument)) || argument === "" ? argument : Number(argument);
|
|
1996
|
+
let fakeFunc = faker;
|
|
1997
|
+
faking.forEach((f) => {
|
|
1998
|
+
fakeFunc = fakeFunc[f];
|
|
1999
|
+
});
|
|
2000
|
+
const newValue = fakeFunc(argument);
|
|
2001
|
+
newFakeParams[key] = newValue;
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
newFakeParams[key] = params[key];
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
return newFakeParams;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
async getBrowserState() {
|
|
2011
|
+
try {
|
|
2012
|
+
const state = await this.browserEmitter?.getState();
|
|
2013
|
+
this.sendEvent(this.events.browserStateSync, state);
|
|
2014
|
+
} catch (error) {
|
|
2015
|
+
this.logger.error("Error getting browser state:", error);
|
|
2016
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2017
|
+
message: "Error getting browser state",
|
|
2018
|
+
code: "GET_STATE_ERROR",
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
async applyClipboardPayload(message) {
|
|
2024
|
+
const payload = message?.data ?? message;
|
|
2025
|
+
|
|
2026
|
+
this.logger.info("[BVTRecorder] applyClipboardPayload called", {
|
|
2027
|
+
hasPayload: !!payload,
|
|
2028
|
+
hasText: !!payload?.text,
|
|
2029
|
+
hasHtml: !!payload?.html,
|
|
2030
|
+
trigger: message?.trigger,
|
|
2031
|
+
});
|
|
2032
|
+
|
|
2033
|
+
if (!payload) {
|
|
2034
|
+
this.logger.warn("[BVTRecorder] No payload provided");
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
try {
|
|
2039
|
+
if (this.browserEmitter && typeof this.browserEmitter.applyClipboardPayload === "function") {
|
|
2040
|
+
this.logger.info("[BVTRecorder] Using RemoteBrowserService to apply clipboard");
|
|
2041
|
+
await this.browserEmitter.applyClipboardPayload(payload);
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
2046
|
+
if (!activePage) {
|
|
2047
|
+
this.logger.warn("[BVTRecorder] No active page available");
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
this.logger.info("[BVTRecorder] Applying clipboard to page", {
|
|
2052
|
+
url: activePage.url(),
|
|
2053
|
+
isClosed: activePage.isClosed(),
|
|
2054
|
+
});
|
|
2055
|
+
|
|
2056
|
+
const result = await activePage.evaluate((clipboardData) => {
|
|
2057
|
+
console.log("[Page] Executing clipboard application", clipboardData);
|
|
2058
|
+
if (typeof window.__bvt_applyClipboardData === "function") {
|
|
2059
|
+
return window.__bvt_applyClipboardData(clipboardData);
|
|
2060
|
+
}
|
|
2061
|
+
console.error("[Page] __bvt_applyClipboardData function not found!");
|
|
2062
|
+
return false;
|
|
2063
|
+
}, payload);
|
|
2064
|
+
|
|
2065
|
+
this.logger.info("[BVTRecorder] Clipboard application result:", result);
|
|
2066
|
+
|
|
2067
|
+
if (!result) {
|
|
2068
|
+
this.logger.warn("[BVTRecorder] Clipboard data not applied successfully");
|
|
2069
|
+
} else {
|
|
2070
|
+
this.logger.info("[BVTRecorder] Clipboard data applied successfully");
|
|
2071
|
+
}
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
this.logger.error("[BVTRecorder] Error applying clipboard payload", error);
|
|
2074
|
+
this.sendEvent(this.events.clipboardError, {
|
|
2075
|
+
message: "Failed to apply clipboard contents to the remote session",
|
|
2076
|
+
trigger: message?.trigger ?? "paste",
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
hasClipboardPayload(payload) {
|
|
2082
|
+
return Boolean(
|
|
2083
|
+
payload && (payload.text || payload.html || (Array.isArray(payload.files) && payload.files.length > 0))
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
async collectClipboardFromPage(page) {
|
|
2088
|
+
if (!page) {
|
|
2089
|
+
this.logger.warn("[BVTRecorder] No page available to collect clipboard data");
|
|
2090
|
+
return null;
|
|
2091
|
+
}
|
|
2092
|
+
try {
|
|
2093
|
+
await page
|
|
2094
|
+
.context()
|
|
2095
|
+
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
2096
|
+
.catch((error) => {
|
|
2097
|
+
this.logger.warn("[BVTRecorder] Failed to grant clipboard permissions before read", error);
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
const payload = await page.evaluate(async () => {
|
|
2101
|
+
const result = {};
|
|
2102
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
|
2103
|
+
return result;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
const arrayBufferToBase64 = (buffer) => {
|
|
2107
|
+
let binary = "";
|
|
2108
|
+
const bytes = new Uint8Array(buffer);
|
|
2109
|
+
const chunkSize = 0x8000;
|
|
2110
|
+
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
2111
|
+
const chunk = bytes.subarray(index, index + chunkSize);
|
|
2112
|
+
binary += String.fromCharCode(...chunk);
|
|
2113
|
+
}
|
|
2114
|
+
return btoa(binary);
|
|
2115
|
+
};
|
|
2116
|
+
|
|
2117
|
+
const files = [];
|
|
2118
|
+
|
|
2119
|
+
if (typeof navigator.clipboard.read === "function") {
|
|
2120
|
+
try {
|
|
2121
|
+
const items = await navigator.clipboard.read();
|
|
2122
|
+
for (const item of items) {
|
|
2123
|
+
if (item.types.includes("text/html") && !result.html) {
|
|
2124
|
+
const blob = await item.getType("text/html");
|
|
2125
|
+
result.html = await blob.text();
|
|
2126
|
+
}
|
|
2127
|
+
if (item.types.includes("text/plain") && !result.text) {
|
|
2128
|
+
const blob = await item.getType("text/plain");
|
|
2129
|
+
result.text = await blob.text();
|
|
2130
|
+
}
|
|
2131
|
+
for (const type of item.types) {
|
|
2132
|
+
if (type.startsWith("text/")) {
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
try {
|
|
2136
|
+
const blob = await item.getType(type);
|
|
2137
|
+
const buffer = await blob.arrayBuffer();
|
|
2138
|
+
files.push({
|
|
2139
|
+
name: `clipboard-file-${files.length + 1}`,
|
|
2140
|
+
type,
|
|
2141
|
+
lastModified: Date.now(),
|
|
2142
|
+
data: arrayBufferToBase64(buffer),
|
|
2143
|
+
});
|
|
2144
|
+
} catch (error) {
|
|
2145
|
+
console.warn("[BVTRecorder] Failed to serialize clipboard blob", { type, error });
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
console.warn("[BVTRecorder] navigator.clipboard.read failed", error);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
if (!result.text && typeof navigator.clipboard.readText === "function") {
|
|
2155
|
+
try {
|
|
2156
|
+
const text = await navigator.clipboard.readText();
|
|
2157
|
+
if (text) {
|
|
2158
|
+
result.text = text;
|
|
2159
|
+
}
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
console.warn("[BVTRecorder] navigator.clipboard.readText failed", error);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
if (!result.text) {
|
|
2166
|
+
const selection = window.getSelection?.()?.toString?.();
|
|
2167
|
+
if (selection) {
|
|
2168
|
+
result.text = selection;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
if (files.length > 0) {
|
|
2173
|
+
result.files = files;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
return result;
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
return payload;
|
|
2180
|
+
} catch (error) {
|
|
2181
|
+
this.logger.error("[BVTRecorder] Error collecting clipboard payload", error);
|
|
2182
|
+
return null;
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
async readClipboardPayload(message) {
|
|
2187
|
+
try {
|
|
2188
|
+
let payload = null;
|
|
2189
|
+
if (this.browserEmitter && typeof this.browserEmitter.readClipboardPayload === "function") {
|
|
2190
|
+
payload = await this.browserEmitter.readClipboardPayload();
|
|
2191
|
+
} else {
|
|
2192
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
2193
|
+
payload = await this.collectClipboardFromPage(activePage);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
if (this.hasClipboardPayload(payload)) {
|
|
2197
|
+
this.logger.info("[BVTRecorder] Remote clipboard payload ready", {
|
|
2198
|
+
hasText: !!payload.text,
|
|
2199
|
+
hasHtml: !!payload.html,
|
|
2200
|
+
files: payload.files?.length ?? 0,
|
|
2201
|
+
});
|
|
2202
|
+
this.sendEvent(this.events.clipboardPush, {
|
|
2203
|
+
data: payload,
|
|
2204
|
+
trigger: message?.trigger ?? "copy",
|
|
2205
|
+
origin: message?.source ?? "browserUI",
|
|
2206
|
+
});
|
|
2207
|
+
return payload;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
this.logger.warn("[BVTRecorder] Remote clipboard payload empty or unavailable");
|
|
2211
|
+
this.sendEvent(this.events.clipboardError, {
|
|
2212
|
+
message: "Remote clipboard is empty",
|
|
2213
|
+
trigger: message?.trigger ?? "copy",
|
|
2214
|
+
});
|
|
2215
|
+
return null;
|
|
2216
|
+
} catch (error) {
|
|
2217
|
+
this.logger.error("[BVTRecorder] Error reading clipboard payload", error);
|
|
2218
|
+
this.sendEvent(this.events.clipboardError, {
|
|
2219
|
+
message: "Failed to read clipboard contents from the remote session",
|
|
2220
|
+
trigger: message?.trigger ?? "copy",
|
|
2221
|
+
details: error instanceof Error ? error.message : String(error),
|
|
2222
|
+
});
|
|
2223
|
+
throw error;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
async injectClipboardIntoPage(page, payload) {
|
|
2228
|
+
if (!page) {
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
try {
|
|
2233
|
+
await page
|
|
2234
|
+
.context()
|
|
2235
|
+
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
2236
|
+
.catch(() => { });
|
|
2237
|
+
await page.evaluate(async (clipboardPayload) => {
|
|
2238
|
+
const toArrayBuffer = (base64) => {
|
|
2239
|
+
if (!base64) {
|
|
2240
|
+
return null;
|
|
2241
|
+
}
|
|
2242
|
+
const binaryString = atob(base64);
|
|
2243
|
+
const len = binaryString.length;
|
|
2244
|
+
const bytes = new Uint8Array(len);
|
|
2245
|
+
for (let i = 0; i < len; i += 1) {
|
|
2246
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
2247
|
+
}
|
|
2248
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
2249
|
+
};
|
|
2250
|
+
|
|
2251
|
+
const createFileFromPayload = (filePayload) => {
|
|
2252
|
+
const buffer = toArrayBuffer(filePayload?.data);
|
|
2253
|
+
if (!buffer) {
|
|
2254
|
+
return null;
|
|
2255
|
+
}
|
|
2256
|
+
const name = filePayload?.name || "clipboard-file";
|
|
2257
|
+
const type = filePayload?.type || "application/octet-stream";
|
|
2258
|
+
const lastModified = filePayload?.lastModified ?? Date.now();
|
|
2259
|
+
try {
|
|
2260
|
+
return new File([buffer], name, { type, lastModified });
|
|
2261
|
+
} catch (error) {
|
|
2262
|
+
console.warn("Clipboard bridge could not recreate File object", error);
|
|
2263
|
+
return null;
|
|
2264
|
+
}
|
|
2265
|
+
};
|
|
2266
|
+
|
|
2267
|
+
let dataTransfer = null;
|
|
2268
|
+
try {
|
|
2269
|
+
dataTransfer = new DataTransfer();
|
|
2270
|
+
} catch (error) {
|
|
2271
|
+
console.warn("Clipboard bridge could not create DataTransfer", error);
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
if (dataTransfer) {
|
|
2275
|
+
if (clipboardPayload?.text) {
|
|
2276
|
+
try {
|
|
2277
|
+
dataTransfer.setData("text/plain", clipboardPayload.text);
|
|
2278
|
+
} catch (error) {
|
|
2279
|
+
console.warn("Clipboard bridge failed to set text/plain", error);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
if (clipboardPayload?.html) {
|
|
2283
|
+
try {
|
|
2284
|
+
dataTransfer.setData("text/html", clipboardPayload.html);
|
|
2285
|
+
} catch (error) {
|
|
2286
|
+
console.warn("Clipboard bridge failed to set text/html", error);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
if (Array.isArray(clipboardPayload?.files)) {
|
|
2290
|
+
for (const filePayload of clipboardPayload.files) {
|
|
2291
|
+
const file = createFileFromPayload(filePayload);
|
|
2292
|
+
if (file) {
|
|
2293
|
+
try {
|
|
2294
|
+
dataTransfer.items.add(file);
|
|
2295
|
+
} catch (error) {
|
|
2296
|
+
console.warn("Clipboard bridge failed to append file", error);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
let target = document.activeElement || document.body;
|
|
2304
|
+
if (!target) {
|
|
2305
|
+
target = document.body || null;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
let pasteHandled = false;
|
|
2309
|
+
if (dataTransfer && target && typeof target.dispatchEvent === "function") {
|
|
2310
|
+
try {
|
|
2311
|
+
const clipboardEvent = new ClipboardEvent("paste", {
|
|
2312
|
+
clipboardData: dataTransfer,
|
|
2313
|
+
bubbles: true,
|
|
2314
|
+
cancelable: true,
|
|
2315
|
+
});
|
|
2316
|
+
pasteHandled = target.dispatchEvent(clipboardEvent);
|
|
2317
|
+
} catch (error) {
|
|
2318
|
+
console.warn("Clipboard bridge failed to dispatch synthetic paste event", error);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
if (pasteHandled) {
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
const callLegacyExecCommand = (command, value) => {
|
|
2327
|
+
const execCommand = document && document["execCommand"];
|
|
2328
|
+
if (typeof execCommand === "function") {
|
|
2329
|
+
try {
|
|
2330
|
+
return execCommand.call(document, command, false, value);
|
|
2331
|
+
} catch (error) {
|
|
2332
|
+
console.warn("Clipboard bridge failed to execute legacy command", error);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
return false;
|
|
2336
|
+
};
|
|
2337
|
+
|
|
2338
|
+
if (clipboardPayload?.html) {
|
|
2339
|
+
const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
|
|
2340
|
+
if (inserted) {
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
try {
|
|
2344
|
+
const selection = window.getSelection?.();
|
|
2345
|
+
if (selection && selection.rangeCount > 0) {
|
|
2346
|
+
const range = selection.getRangeAt(0);
|
|
2347
|
+
range.deleteContents();
|
|
2348
|
+
const fragment = range.createContextualFragment(clipboardPayload.html);
|
|
2349
|
+
range.insertNode(fragment);
|
|
2350
|
+
range.collapse(false);
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
} catch (error) {
|
|
2354
|
+
console.warn("Clipboard bridge could not insert HTML via Range APIs", error);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
if (clipboardPayload?.text) {
|
|
2359
|
+
const inserted = callLegacyExecCommand("insertText", clipboardPayload.text);
|
|
2360
|
+
if (inserted) {
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
try {
|
|
2364
|
+
const selection = window.getSelection?.();
|
|
2365
|
+
if (selection && selection.rangeCount > 0) {
|
|
2366
|
+
const range = selection.getRangeAt(0);
|
|
2367
|
+
range.deleteContents();
|
|
2368
|
+
range.insertNode(document.createTextNode(clipboardPayload.text));
|
|
2369
|
+
range.collapse(false);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
} catch (error) {
|
|
2373
|
+
console.warn("Clipboard bridge could not insert text via Range APIs", error);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
if (clipboardPayload?.text && target && "value" in target) {
|
|
2378
|
+
try {
|
|
2379
|
+
const input = target;
|
|
2380
|
+
const start = input.selectionStart ?? input.value.length ?? 0;
|
|
2381
|
+
const end = input.selectionEnd ?? input.value.length ?? 0;
|
|
2382
|
+
const value = input.value ?? "";
|
|
2383
|
+
const text = clipboardPayload.text;
|
|
2384
|
+
input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
|
|
2385
|
+
const caret = start + text.length;
|
|
2386
|
+
if (typeof input.setSelectionRange === "function") {
|
|
2387
|
+
input.setSelectionRange(caret, caret);
|
|
2388
|
+
}
|
|
2389
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2390
|
+
} catch (error) {
|
|
2391
|
+
console.warn("Clipboard bridge failed to mutate input element", error);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
}, payload);
|
|
2395
|
+
} catch (error) {
|
|
2396
|
+
throw error;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
async createTab(url) {
|
|
2401
|
+
try {
|
|
2402
|
+
await this.browserEmitter?.createTab(url);
|
|
2403
|
+
} catch (error) {
|
|
2404
|
+
this.logger.error("Error creating tab:", error);
|
|
2405
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2406
|
+
message: "Error creating tab",
|
|
2407
|
+
code: "CREATE_TAB_ERROR",
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
async closeTab(pageId) {
|
|
2413
|
+
try {
|
|
2414
|
+
await this.browserEmitter?.closeTab(pageId);
|
|
2415
|
+
} catch (error) {
|
|
2416
|
+
this.logger.error("Error closing tab:", error);
|
|
2417
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2418
|
+
message: "Error closing tab",
|
|
2419
|
+
code: "CLOSE_TAB_ERROR",
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
async selectTab(pageId) {
|
|
2425
|
+
try {
|
|
2426
|
+
await this.browserEmitter?.selectTab(pageId);
|
|
2427
|
+
} catch (error) {
|
|
2428
|
+
this.logger.error("Error selecting tab:", error);
|
|
2429
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2430
|
+
message: "Error selecting tab",
|
|
2431
|
+
code: "SELECT_TAB_ERROR",
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
async navigateTab({ pageId, url }) {
|
|
2437
|
+
try {
|
|
2438
|
+
if (!pageId || !url) {
|
|
2439
|
+
this.logger.error("navigateTab called without pageId or url", { pageId, url });
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
await this.browserEmitter?.navigateTab(pageId, url);
|
|
2443
|
+
} catch (error) {
|
|
2444
|
+
this.logger.error("Error navigating tab:", error);
|
|
2445
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2446
|
+
message: "Error navigating tab",
|
|
2447
|
+
code: "NAVIGATE_TAB_ERROR",
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
async reloadTab(pageId) {
|
|
2453
|
+
try {
|
|
2454
|
+
await this.browserEmitter?.reloadTab(pageId);
|
|
2455
|
+
} catch (error) {
|
|
2456
|
+
this.logger.error("Error reloading tab:", error);
|
|
2457
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2458
|
+
message: "Error reloading tab",
|
|
2459
|
+
code: "RELOAD_TAB_ERROR",
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
async goBack(pageId) {
|
|
2465
|
+
try {
|
|
2466
|
+
await this.browserEmitter?.goBack(pageId);
|
|
2467
|
+
} catch (error) {
|
|
2468
|
+
this.logger.error("Error navigating back:", error);
|
|
2469
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2470
|
+
message: "Error navigating back",
|
|
2471
|
+
code: "GO_BACK_ERROR",
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
async goForward(pageId) {
|
|
2477
|
+
try {
|
|
2478
|
+
await this.browserEmitter?.goForward(pageId);
|
|
2479
|
+
} catch (error) {
|
|
2480
|
+
this.logger.error("Error navigating forward:", error);
|
|
2481
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2482
|
+
message: "Error navigating forward",
|
|
2483
|
+
code: "GO_FORWARD_ERROR",
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
1238
2487
|
}
|