@dev-blinq/cucumber_client 1.0.1455-dev → 1.0.1455-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 +52 -11
- package/bin/client/code_gen/playwright_codeget.js +164 -75
- 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 +305 -0
- package/bin/client/recorderv3/bvt_recorder.js +1031 -61
- package/bin/client/recorderv3/implemented_steps.js +2 -0
- package/bin/client/recorderv3/index.js +3 -286
- package/bin/client/recorderv3/services.js +818 -142
- package/bin/client/recorderv3/step_runner.js +21 -4
- package/bin/client/recorderv3/step_utils.js +194 -118
- 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,333 @@
|
|
|
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 path from "path";
|
|
5
|
-
import url from "url";
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import url from "node:url";
|
|
6
6
|
import { getImplementedSteps, parseRouteFiles } from "./implemented_steps.js";
|
|
7
|
-
import { NamesService } from "./services.js";
|
|
7
|
+
import { NamesService, PublishService } from "./services.js";
|
|
8
8
|
import { BVTStepRunner } from "./step_runner.js";
|
|
9
|
-
import { readFile, writeFile } from "fs/promises";
|
|
10
|
-
import {
|
|
11
|
-
import { updateFeatureFile } from "./update_feature.js";
|
|
9
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
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
|
-
import socketLogger from "../utils/socket_logger.js";
|
|
16
|
+
import socketLogger, { getErrorMessage } from "../utils/socket_logger.js";
|
|
18
17
|
import { tmpdir } from "os";
|
|
18
|
+
import { faker } from "@faker-js/faker/locale/en_US";
|
|
19
|
+
import { chromium } from "playwright-core";
|
|
20
|
+
|
|
19
21
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
20
22
|
|
|
21
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
|
+
|
|
22
331
|
export function getInitScript(config, options) {
|
|
23
332
|
const preScript = `
|
|
24
333
|
window.__bvt_Recorder_config = ${JSON.stringify(config ?? null)};
|
|
@@ -28,7 +337,7 @@ export function getInitScript(config, options) {
|
|
|
28
337
|
path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"),
|
|
29
338
|
"utf8"
|
|
30
339
|
);
|
|
31
|
-
return preScript + recorderScript;
|
|
340
|
+
return preScript + recorderScript + clipboardBridgeScript;
|
|
32
341
|
}
|
|
33
342
|
|
|
34
343
|
async function evaluate(frame, script) {
|
|
@@ -42,7 +351,6 @@ async function evaluate(frame, script) {
|
|
|
42
351
|
}
|
|
43
352
|
|
|
44
353
|
async function findNestedFrameSelector(frame, obj) {
|
|
45
|
-
console.log("Testing new version");
|
|
46
354
|
try {
|
|
47
355
|
const parent = frame.parentFrame();
|
|
48
356
|
if (!parent) return { children: obj };
|
|
@@ -53,8 +361,7 @@ async function findNestedFrameSelector(frame, obj) {
|
|
|
53
361
|
}, frameElement);
|
|
54
362
|
return findNestedFrameSelector(parent, { children: obj, selectors });
|
|
55
363
|
} catch (e) {
|
|
56
|
-
socketLogger.error(`Error in
|
|
57
|
-
console.error(e);
|
|
364
|
+
socketLogger.error(`Error in script evaluation: ${getErrorMessage(e)}`, undefined, "findNestedFrameSelector");
|
|
58
365
|
}
|
|
59
366
|
}
|
|
60
367
|
const transformFillAction = (action, el) => {
|
|
@@ -156,6 +463,17 @@ const transformAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode,
|
|
|
156
463
|
}
|
|
157
464
|
}
|
|
158
465
|
};
|
|
466
|
+
const diffPaths = (currentPath, newPath) => {
|
|
467
|
+
const currentDomain = new URL(currentPath).hostname;
|
|
468
|
+
const newDomain = new URL(newPath).hostname;
|
|
469
|
+
if (currentDomain !== newDomain) {
|
|
470
|
+
return true;
|
|
471
|
+
} else {
|
|
472
|
+
const currentRoute = new URL(currentPath).pathname;
|
|
473
|
+
const newRoute = new URL(newPath).pathname;
|
|
474
|
+
return currentRoute !== newRoute;
|
|
475
|
+
}
|
|
476
|
+
};
|
|
159
477
|
/**
|
|
160
478
|
* @typedef {Object} BVTRecorderInput
|
|
161
479
|
* @property {string} envName
|
|
@@ -185,12 +503,16 @@ export class BVTRecorder {
|
|
|
185
503
|
projectDir: this.projectDir,
|
|
186
504
|
logger: this.logger,
|
|
187
505
|
});
|
|
506
|
+
this.workspaceService = new PublishService(this.TOKEN);
|
|
188
507
|
this.pageSet = new Set();
|
|
189
508
|
this.lastKnownUrlPath = "";
|
|
190
|
-
this.world = { attach: () => {} };
|
|
509
|
+
this.world = { attach: () => { } };
|
|
191
510
|
this.shouldTakeScreenshot = true;
|
|
192
511
|
this.watcher = null;
|
|
193
512
|
this.networkEventsFolder = path.join(tmpdir(), "blinq_network_events");
|
|
513
|
+
this.tempProjectFolder = `${tmpdir()}/bvt_temp_project_${Math.floor(Math.random() * 1000000)}`;
|
|
514
|
+
this.tempSnapshotsFolder = path.join(this.tempProjectFolder, "data/snapshots");
|
|
515
|
+
|
|
194
516
|
if (existsSync(this.networkEventsFolder)) {
|
|
195
517
|
rmSync(this.networkEventsFolder, { recursive: true, force: true });
|
|
196
518
|
}
|
|
@@ -208,6 +530,12 @@ export class BVTRecorder {
|
|
|
208
530
|
cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
|
|
209
531
|
cmdExecutionError: "BVTRecorder.cmdExecutionError",
|
|
210
532
|
interceptResults: "BVTRecorder.interceptResults",
|
|
533
|
+
onDebugURLChange: "BVTRecorder.onDebugURLChange",
|
|
534
|
+
updateCommand: "BVTRecorder.updateCommand",
|
|
535
|
+
browserStateSync: "BrowserService.stateSync",
|
|
536
|
+
browserStateError: "BrowserService.stateError",
|
|
537
|
+
clipboardPush: "BrowserService.clipboardPush",
|
|
538
|
+
clipboardError: "BrowserService.clipboardError",
|
|
211
539
|
};
|
|
212
540
|
bindings = {
|
|
213
541
|
__bvt_recordCommand: async ({ frame, page, context }, event) => {
|
|
@@ -237,6 +565,25 @@ export class BVTRecorder {
|
|
|
237
565
|
__bvt_getObject: (_src, obj) => {
|
|
238
566
|
this.processObject(obj);
|
|
239
567
|
},
|
|
568
|
+
__bvt_reportClipboard: async ({ page }, payload) => {
|
|
569
|
+
try {
|
|
570
|
+
if (!payload) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
574
|
+
if (activePage && activePage !== page) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const pageUrl = typeof page?.url === "function" ? page.url() : null;
|
|
578
|
+
this.sendEvent(this.events.clipboardPush, {
|
|
579
|
+
data: payload,
|
|
580
|
+
trigger: payload?.trigger ?? "copy",
|
|
581
|
+
pageUrl,
|
|
582
|
+
});
|
|
583
|
+
} catch (error) {
|
|
584
|
+
this.logger.error("Error forwarding clipboard payload from page", error);
|
|
585
|
+
}
|
|
586
|
+
},
|
|
240
587
|
};
|
|
241
588
|
|
|
242
589
|
getSnapshot = async (attr) => {
|
|
@@ -294,11 +641,15 @@ export class BVTRecorder {
|
|
|
294
641
|
}
|
|
295
642
|
|
|
296
643
|
async _initBrowser({ url }) {
|
|
297
|
-
|
|
298
|
-
|
|
644
|
+
if (process.env.CDP_LISTEN_PORT === undefined) {
|
|
645
|
+
this.#remoteDebuggerPort = await findAvailablePort();
|
|
646
|
+
process.env.CDP_LISTEN_PORT = this.#remoteDebuggerPort;
|
|
647
|
+
} else {
|
|
648
|
+
this.#remoteDebuggerPort = process.env.CDP_LISTEN_PORT;
|
|
649
|
+
}
|
|
299
650
|
|
|
300
651
|
// this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
|
|
301
|
-
this.world = { attach: () => {} };
|
|
652
|
+
this.world = { attach: () => { } };
|
|
302
653
|
|
|
303
654
|
const ai_config_file = path.join(this.projectDir, "ai_config.json");
|
|
304
655
|
let ai_config = {};
|
|
@@ -318,7 +669,8 @@ export class BVTRecorder {
|
|
|
318
669
|
],
|
|
319
670
|
};
|
|
320
671
|
|
|
321
|
-
const
|
|
672
|
+
const scenario = { pickle: this.scenarioDoc };
|
|
673
|
+
const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName, scenario);
|
|
322
674
|
this.bvtContext = bvtContext;
|
|
323
675
|
this.stepRunner = new BVTStepRunner({
|
|
324
676
|
projectDir: this.projectDir,
|
|
@@ -344,8 +696,7 @@ export class BVTRecorder {
|
|
|
344
696
|
},
|
|
345
697
|
bvtContext: this.bvtContext,
|
|
346
698
|
});
|
|
347
|
-
|
|
348
|
-
this.context = context;
|
|
699
|
+
this.context = bvtContext.playContext;
|
|
349
700
|
this.web = bvtContext.stable || bvtContext.web;
|
|
350
701
|
this.web.tryAllStrategies = true;
|
|
351
702
|
this.page = bvtContext.page;
|
|
@@ -359,9 +710,19 @@ export class BVTRecorder {
|
|
|
359
710
|
await this.context.exposeBinding(name, handler);
|
|
360
711
|
}
|
|
361
712
|
this._watchTestData();
|
|
362
|
-
this.web.onRestoreSaveState = (url) => {
|
|
363
|
-
this._initBrowser({ url });
|
|
713
|
+
this.web.onRestoreSaveState = async (url) => {
|
|
714
|
+
await this._initBrowser({ url });
|
|
715
|
+
this._addPagelisteners(this.context);
|
|
716
|
+
this._addFrameNavigateListener(this.page);
|
|
364
717
|
};
|
|
718
|
+
|
|
719
|
+
// create a second browser for locator generation
|
|
720
|
+
this.backgroundBrowser = await chromium.launch({
|
|
721
|
+
headless: true,
|
|
722
|
+
});
|
|
723
|
+
this.backgroundContext = await this.backgroundBrowser.newContext({});
|
|
724
|
+
await this.backgroundContext.addInitScript({ content: this.getInitScripts(this.config) });
|
|
725
|
+
await this.backgroundContext.newPage();
|
|
365
726
|
}
|
|
366
727
|
async onClosePopup() {
|
|
367
728
|
// console.log("close popups");
|
|
@@ -403,13 +764,14 @@ export class BVTRecorder {
|
|
|
403
764
|
|
|
404
765
|
await this.page.goto(url, {
|
|
405
766
|
waitUntil: "domcontentloaded",
|
|
767
|
+
timeout: this.config.page_timeout ?? 60_000,
|
|
406
768
|
});
|
|
407
769
|
// add listener for frame navigation on current tab
|
|
408
770
|
this._addFrameNavigateListener(this.page);
|
|
409
771
|
|
|
410
772
|
// eval init script on current tab
|
|
411
773
|
// await this._initPage(this.page);
|
|
412
|
-
this.#currentURL =
|
|
774
|
+
this.#currentURL = url;
|
|
413
775
|
|
|
414
776
|
await this.page.dispatchEvent("html", "scroll");
|
|
415
777
|
await delay(1000);
|
|
@@ -451,14 +813,15 @@ export class BVTRecorder {
|
|
|
451
813
|
element: { inputID: "frame" },
|
|
452
814
|
});
|
|
453
815
|
|
|
454
|
-
const
|
|
816
|
+
const newUrl = frame.url();
|
|
817
|
+
const newPath = new URL(newUrl).pathname;
|
|
455
818
|
const newTitle = await frame.title();
|
|
456
|
-
|
|
819
|
+
const changed = diffPaths(this.#currentURL, newUrl);
|
|
820
|
+
|
|
821
|
+
if (changed) {
|
|
457
822
|
this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
|
|
458
|
-
this.#currentURL =
|
|
823
|
+
this.#currentURL = newUrl;
|
|
459
824
|
}
|
|
460
|
-
// await this._setRecordingMode(frame);
|
|
461
|
-
// await this._initPage(page);
|
|
462
825
|
} catch (error) {
|
|
463
826
|
this.logger.error("Error in frame navigate event");
|
|
464
827
|
this.logger.error(error);
|
|
@@ -623,7 +986,6 @@ export class BVTRecorder {
|
|
|
623
986
|
try {
|
|
624
987
|
if (page.isClosed()) return;
|
|
625
988
|
this.pageSet.add(page);
|
|
626
|
-
|
|
627
989
|
await page.waitForLoadState("domcontentloaded");
|
|
628
990
|
|
|
629
991
|
// add listener for frame navigation on new tab
|
|
@@ -688,6 +1050,52 @@ export class BVTRecorder {
|
|
|
688
1050
|
console.error("Error in saving screenshot: ", error);
|
|
689
1051
|
}
|
|
690
1052
|
}
|
|
1053
|
+
async generateLocators(event) {
|
|
1054
|
+
const snapshotDetails = event.snapshotDetails;
|
|
1055
|
+
if (!snapshotDetails) {
|
|
1056
|
+
throw new Error("No snapshot details found");
|
|
1057
|
+
}
|
|
1058
|
+
const mode = event.mode;
|
|
1059
|
+
const inputID = event.element.inputID;
|
|
1060
|
+
|
|
1061
|
+
const { id, contextId, doc } = snapshotDetails;
|
|
1062
|
+
// const selector = `[data-blinq-id="${id}"]`;
|
|
1063
|
+
const newPage = await this.backgroundContext.newPage();
|
|
1064
|
+
await newPage.setContent(doc, { waitUntil: "domcontentloaded" });
|
|
1065
|
+
const locatorsObj = await newPage.evaluate(
|
|
1066
|
+
([id, contextId, mode]) => {
|
|
1067
|
+
const recorder = window.__bvt_Recorder;
|
|
1068
|
+
const contextElement = document.querySelector(`[data-blinq-context-id="${contextId}"]`);
|
|
1069
|
+
const el = document.querySelector(`[data-blinq-id="${id}"]`);
|
|
1070
|
+
if (contextElement) {
|
|
1071
|
+
const result = recorder.locatorGenerator.toContextLocators(el, contextElement);
|
|
1072
|
+
return result;
|
|
1073
|
+
}
|
|
1074
|
+
const isRecordingText = mode === "recordingText";
|
|
1075
|
+
return recorder.locatorGenerator.getElementLocators(el, {
|
|
1076
|
+
excludeText: isRecordingText,
|
|
1077
|
+
});
|
|
1078
|
+
},
|
|
1079
|
+
[id, contextId, mode]
|
|
1080
|
+
);
|
|
1081
|
+
|
|
1082
|
+
// console.log(`Generated locators: for ${inputID}: `, JSON.stringify(locatorsObj));
|
|
1083
|
+
await newPage.close();
|
|
1084
|
+
if (event.nestFrmLoc?.children) {
|
|
1085
|
+
locatorsObj.nestFrmLoc = event.nestFrmLoc.children;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
this.sendEvent(this.events.updateCommand, {
|
|
1089
|
+
locators: {
|
|
1090
|
+
locators: locatorsObj.locators,
|
|
1091
|
+
nestFrmLoc: locatorsObj.nestFrmLoc,
|
|
1092
|
+
iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
1093
|
+
},
|
|
1094
|
+
allStrategyLocators: locatorsObj.allStrategyLocators,
|
|
1095
|
+
inputID,
|
|
1096
|
+
});
|
|
1097
|
+
// const
|
|
1098
|
+
}
|
|
691
1099
|
async onAction(event) {
|
|
692
1100
|
this._updateUrlPath();
|
|
693
1101
|
// const locators = this.overlayLocators(event);
|
|
@@ -701,25 +1109,41 @@ export class BVTRecorder {
|
|
|
701
1109
|
event.mode === "recordingHover",
|
|
702
1110
|
event.mode === "multiInspecting"
|
|
703
1111
|
),
|
|
704
|
-
locators: {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
},
|
|
708
|
-
allStrategyLocators: event.allStrategyLocators,
|
|
1112
|
+
// locators: {
|
|
1113
|
+
// locators: event.locators,
|
|
1114
|
+
// iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
1115
|
+
// },
|
|
1116
|
+
// allStrategyLocators: event.allStrategyLocators,
|
|
709
1117
|
url: event.frame.url,
|
|
710
1118
|
title: event.frame.title,
|
|
711
1119
|
extract: {},
|
|
712
1120
|
lastKnownUrlPath: this.lastKnownUrlPath,
|
|
713
1121
|
};
|
|
714
|
-
if (event.nestFrmLoc?.children) {
|
|
715
|
-
|
|
716
|
-
}
|
|
1122
|
+
// if (event.nestFrmLoc?.children) {
|
|
1123
|
+
// cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
|
|
1124
|
+
// }
|
|
717
1125
|
// this.logger.info({ event });
|
|
718
1126
|
if (this.shouldTakeScreenshot) {
|
|
719
1127
|
await this.storeScreenshot(event);
|
|
720
1128
|
}
|
|
721
|
-
this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
722
|
-
this._updateUrlPath();
|
|
1129
|
+
// this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
1130
|
+
// this._updateUrlPath();
|
|
1131
|
+
if (event.locators) {
|
|
1132
|
+
Object.assign(cmdEvent, {
|
|
1133
|
+
locators: {
|
|
1134
|
+
locators: event.locators,
|
|
1135
|
+
iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
1136
|
+
nestFrmLoc: event.nestFrmLoc?.children,
|
|
1137
|
+
},
|
|
1138
|
+
allStrategyLocators: event.allStrategyLocators,
|
|
1139
|
+
})
|
|
1140
|
+
this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
1141
|
+
this._updateUrlPath();
|
|
1142
|
+
} else {
|
|
1143
|
+
this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
1144
|
+
this._updateUrlPath();
|
|
1145
|
+
await this.generateLocators(event);
|
|
1146
|
+
}
|
|
723
1147
|
}
|
|
724
1148
|
_updateUrlPath() {
|
|
725
1149
|
try {
|
|
@@ -735,13 +1159,12 @@ export class BVTRecorder {
|
|
|
735
1159
|
}
|
|
736
1160
|
async closeBrowser() {
|
|
737
1161
|
delete process.env.TEMP_RUN;
|
|
738
|
-
await this.watcher.close().then(() => {});
|
|
1162
|
+
await this.watcher.close().then(() => { });
|
|
739
1163
|
this.watcher = null;
|
|
740
1164
|
this.previousIndex = null;
|
|
741
1165
|
this.previousHistoryLength = null;
|
|
742
1166
|
this.previousUrl = null;
|
|
743
1167
|
this.previousEntries = null;
|
|
744
|
-
|
|
745
1168
|
await closeContext();
|
|
746
1169
|
this.pageSet.clear();
|
|
747
1170
|
}
|
|
@@ -764,25 +1187,26 @@ export class BVTRecorder {
|
|
|
764
1187
|
for (let i = 0; i < 3; i++) {
|
|
765
1188
|
result = 0;
|
|
766
1189
|
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
|
-
|
|
1190
|
+
// for (const page of this.context.pages()) {
|
|
1191
|
+
const page = this.web.page;
|
|
1192
|
+
for (const frame of page.frames()) {
|
|
1193
|
+
try {
|
|
1194
|
+
//scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params: Params)
|
|
1195
|
+
const frameResult = await this.web._locateElementByText(
|
|
1196
|
+
frame,
|
|
1197
|
+
searchString,
|
|
1198
|
+
tag,
|
|
1199
|
+
regex,
|
|
1200
|
+
partial,
|
|
1201
|
+
ignoreCase,
|
|
1202
|
+
{}
|
|
1203
|
+
);
|
|
1204
|
+
result += frameResult.elementCount;
|
|
1205
|
+
} catch (e) {
|
|
1206
|
+
console.log(e);
|
|
784
1207
|
}
|
|
785
1208
|
}
|
|
1209
|
+
// }
|
|
786
1210
|
|
|
787
1211
|
return result;
|
|
788
1212
|
} catch (e) {
|
|
@@ -835,13 +1259,19 @@ export class BVTRecorder {
|
|
|
835
1259
|
}
|
|
836
1260
|
async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
|
|
837
1261
|
const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
|
|
1262
|
+
|
|
1263
|
+
const env = path.basename(this.envName, ".json");
|
|
838
1264
|
const _env = {
|
|
839
1265
|
TOKEN: this.TOKEN,
|
|
840
1266
|
TEMP_RUN: true,
|
|
841
1267
|
REPORT_FOLDER: this.bvtContext.reportFolder,
|
|
842
1268
|
BLINQ_ENV: this.envName,
|
|
843
1269
|
DEBUG: "blinq:route",
|
|
1270
|
+
// BVT_TEMP_SNAPSHOTS_FOLDER: step.isImplemented ? path.join(this.tempSnapshotsFolder, env) : undefined,
|
|
844
1271
|
};
|
|
1272
|
+
if (!step.isImplemented) {
|
|
1273
|
+
_env.BVT_TEMP_SNAPSHOTS_FOLDER = path.join(this.tempSnapshotsFolder, env);
|
|
1274
|
+
}
|
|
845
1275
|
|
|
846
1276
|
this.bvtContext.navigate = true;
|
|
847
1277
|
this.bvtContext.loadedRoutes = null;
|
|
@@ -861,6 +1291,7 @@ export class BVTRecorder {
|
|
|
861
1291
|
await this.setMode("running");
|
|
862
1292
|
|
|
863
1293
|
try {
|
|
1294
|
+
step.text = step.text.trim();
|
|
864
1295
|
const { result, info } = await this.stepRunner.runStep(
|
|
865
1296
|
{
|
|
866
1297
|
step,
|
|
@@ -887,10 +1318,22 @@ export class BVTRecorder {
|
|
|
887
1318
|
this.bvtContext.navigate = false;
|
|
888
1319
|
}
|
|
889
1320
|
}
|
|
890
|
-
async saveScenario({ scenario, featureName, override, isSingleStep }) {
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1321
|
+
async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env, AICode }) {
|
|
1322
|
+
const res = await this.workspaceService.saveScenario({
|
|
1323
|
+
scenario,
|
|
1324
|
+
featureName,
|
|
1325
|
+
override,
|
|
1326
|
+
isSingleStep,
|
|
1327
|
+
branch,
|
|
1328
|
+
isEditing,
|
|
1329
|
+
projectId: path.basename(this.projectDir),
|
|
1330
|
+
env: env ?? this.envName,
|
|
1331
|
+
});
|
|
1332
|
+
if (res.success) {
|
|
1333
|
+
await this.cleanup({ tags: scenario.tags });
|
|
1334
|
+
} else {
|
|
1335
|
+
throw new Error(res.message || "Error saving scenario");
|
|
1336
|
+
}
|
|
894
1337
|
}
|
|
895
1338
|
async getImplementedSteps() {
|
|
896
1339
|
const stepsAndScenarios = await getImplementedSteps(this.projectDir);
|
|
@@ -1055,9 +1498,11 @@ export class BVTRecorder {
|
|
|
1055
1498
|
const featureFilePath = path.join(this.projectDir, "features", featureName);
|
|
1056
1499
|
const gherkinDoc = this.parseFeatureFile(featureFilePath);
|
|
1057
1500
|
const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
|
|
1501
|
+
this.scenarioDoc = scenario;
|
|
1058
1502
|
|
|
1059
1503
|
const steps = [];
|
|
1060
1504
|
const parameters = [];
|
|
1505
|
+
const datasets = [];
|
|
1061
1506
|
if (scenario.examples && scenario.examples.length > 0) {
|
|
1062
1507
|
const example = scenario.examples[0];
|
|
1063
1508
|
example?.tableHeader?.cells.forEach((cell, index) => {
|
|
@@ -1065,7 +1510,26 @@ export class BVTRecorder {
|
|
|
1065
1510
|
key: cell.value,
|
|
1066
1511
|
value: unEscapeNonPrintables(example.tableBody[0].cells[index].value),
|
|
1067
1512
|
});
|
|
1513
|
+
// datasets.push({
|
|
1514
|
+
// data: example.tableBody[]
|
|
1515
|
+
// })
|
|
1068
1516
|
});
|
|
1517
|
+
|
|
1518
|
+
for (let i = 0; i < example.tableBody.length; i++) {
|
|
1519
|
+
const row = example.tableBody[i];
|
|
1520
|
+
// for (const row of example.tableBody) {
|
|
1521
|
+
const paramters = [];
|
|
1522
|
+
row.cells.forEach((cell, index) => {
|
|
1523
|
+
paramters.push({
|
|
1524
|
+
key: example.tableHeader.cells[index].value,
|
|
1525
|
+
value: unEscapeNonPrintables(cell.value),
|
|
1526
|
+
});
|
|
1527
|
+
});
|
|
1528
|
+
datasets.push({
|
|
1529
|
+
data: paramters,
|
|
1530
|
+
datasetId: i,
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1069
1533
|
}
|
|
1070
1534
|
|
|
1071
1535
|
for (const step of scenario.steps) {
|
|
@@ -1086,6 +1550,7 @@ export class BVTRecorder {
|
|
|
1086
1550
|
tags: scenario.tags.map((tag) => tag.name),
|
|
1087
1551
|
steps,
|
|
1088
1552
|
parameters,
|
|
1553
|
+
datasets,
|
|
1089
1554
|
};
|
|
1090
1555
|
}
|
|
1091
1556
|
async findRelatedTextInAllFrames({ searchString, climb, contextText, params }) {
|
|
@@ -1235,4 +1700,509 @@ export class BVTRecorder {
|
|
|
1235
1700
|
this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
|
|
1236
1701
|
}
|
|
1237
1702
|
}
|
|
1703
|
+
|
|
1704
|
+
async fakeParams(params) {
|
|
1705
|
+
const newFakeParams = {};
|
|
1706
|
+
Object.keys(params).forEach((key) => {
|
|
1707
|
+
if (!params[key].startsWith("{{") || !params[key].endsWith("}}")) {
|
|
1708
|
+
newFakeParams[key] = params[key];
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
try {
|
|
1713
|
+
const value = params[key].substring(2, params[key].length - 2).trim();
|
|
1714
|
+
const faking = value.split("(")[0].split(".");
|
|
1715
|
+
let argument = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
|
|
1716
|
+
argument = isNaN(Number(argument)) || argument === "" ? argument : Number(argument);
|
|
1717
|
+
let fakeFunc = faker;
|
|
1718
|
+
faking.forEach((f) => {
|
|
1719
|
+
fakeFunc = fakeFunc[f];
|
|
1720
|
+
});
|
|
1721
|
+
const newValue = fakeFunc(argument);
|
|
1722
|
+
newFakeParams[key] = newValue;
|
|
1723
|
+
} catch (error) {
|
|
1724
|
+
newFakeParams[key] = params[key];
|
|
1725
|
+
}
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
return newFakeParams;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
async getBrowserState() {
|
|
1732
|
+
try {
|
|
1733
|
+
const state = await this.browserEmitter?.getState();
|
|
1734
|
+
this.sendEvent(this.events.browserStateSync, state);
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
this.logger.error("Error getting browser state:", error);
|
|
1737
|
+
this.sendEvent(this.events.browserStateError, {
|
|
1738
|
+
message: "Error getting browser state",
|
|
1739
|
+
code: "GET_STATE_ERROR",
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
async applyClipboardPayload(message) {
|
|
1745
|
+
const payload = message?.data ?? message;
|
|
1746
|
+
|
|
1747
|
+
this.logger.info("[BVTRecorder] applyClipboardPayload called", {
|
|
1748
|
+
hasPayload: !!payload,
|
|
1749
|
+
hasText: !!payload?.text,
|
|
1750
|
+
hasHtml: !!payload?.html,
|
|
1751
|
+
trigger: message?.trigger,
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
if (!payload) {
|
|
1755
|
+
this.logger.warn("[BVTRecorder] No payload provided");
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
try {
|
|
1760
|
+
if (this.browserEmitter && typeof this.browserEmitter.applyClipboardPayload === "function") {
|
|
1761
|
+
this.logger.info("[BVTRecorder] Using RemoteBrowserService to apply clipboard");
|
|
1762
|
+
await this.browserEmitter.applyClipboardPayload(payload);
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
1767
|
+
if (!activePage) {
|
|
1768
|
+
this.logger.warn("[BVTRecorder] No active page available");
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
this.logger.info("[BVTRecorder] Applying clipboard to page", {
|
|
1773
|
+
url: activePage.url(),
|
|
1774
|
+
isClosed: activePage.isClosed(),
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
const result = await activePage.evaluate((clipboardData) => {
|
|
1778
|
+
console.log("[Page] Executing clipboard application", clipboardData);
|
|
1779
|
+
if (typeof window.__bvt_applyClipboardData === "function") {
|
|
1780
|
+
return window.__bvt_applyClipboardData(clipboardData);
|
|
1781
|
+
}
|
|
1782
|
+
console.error("[Page] __bvt_applyClipboardData function not found!");
|
|
1783
|
+
return false;
|
|
1784
|
+
}, payload);
|
|
1785
|
+
|
|
1786
|
+
this.logger.info("[BVTRecorder] Clipboard application result:", result);
|
|
1787
|
+
|
|
1788
|
+
if (!result) {
|
|
1789
|
+
this.logger.warn("[BVTRecorder] Clipboard data not applied successfully");
|
|
1790
|
+
} else {
|
|
1791
|
+
this.logger.info("[BVTRecorder] Clipboard data applied successfully");
|
|
1792
|
+
}
|
|
1793
|
+
} catch (error) {
|
|
1794
|
+
this.logger.error("[BVTRecorder] Error applying clipboard payload", error);
|
|
1795
|
+
this.sendEvent(this.events.clipboardError, {
|
|
1796
|
+
message: "Failed to apply clipboard contents to the remote session",
|
|
1797
|
+
trigger: message?.trigger ?? "paste",
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
hasClipboardPayload(payload) {
|
|
1803
|
+
return Boolean(
|
|
1804
|
+
payload && (payload.text || payload.html || (Array.isArray(payload.files) && payload.files.length > 0))
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
async collectClipboardFromPage(page) {
|
|
1809
|
+
if (!page) {
|
|
1810
|
+
this.logger.warn("[BVTRecorder] No page available to collect clipboard data");
|
|
1811
|
+
return null;
|
|
1812
|
+
}
|
|
1813
|
+
try {
|
|
1814
|
+
await page
|
|
1815
|
+
.context()
|
|
1816
|
+
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
1817
|
+
.catch((error) => {
|
|
1818
|
+
this.logger.warn("[BVTRecorder] Failed to grant clipboard permissions before read", error);
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1821
|
+
const payload = await page.evaluate(async () => {
|
|
1822
|
+
const result = {};
|
|
1823
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
|
1824
|
+
return result;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
const arrayBufferToBase64 = (buffer) => {
|
|
1828
|
+
let binary = "";
|
|
1829
|
+
const bytes = new Uint8Array(buffer);
|
|
1830
|
+
const chunkSize = 0x8000;
|
|
1831
|
+
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
1832
|
+
const chunk = bytes.subarray(index, index + chunkSize);
|
|
1833
|
+
binary += String.fromCharCode(...chunk);
|
|
1834
|
+
}
|
|
1835
|
+
return btoa(binary);
|
|
1836
|
+
};
|
|
1837
|
+
|
|
1838
|
+
const files = [];
|
|
1839
|
+
|
|
1840
|
+
if (typeof navigator.clipboard.read === "function") {
|
|
1841
|
+
try {
|
|
1842
|
+
const items = await navigator.clipboard.read();
|
|
1843
|
+
for (const item of items) {
|
|
1844
|
+
if (item.types.includes("text/html") && !result.html) {
|
|
1845
|
+
const blob = await item.getType("text/html");
|
|
1846
|
+
result.html = await blob.text();
|
|
1847
|
+
}
|
|
1848
|
+
if (item.types.includes("text/plain") && !result.text) {
|
|
1849
|
+
const blob = await item.getType("text/plain");
|
|
1850
|
+
result.text = await blob.text();
|
|
1851
|
+
}
|
|
1852
|
+
for (const type of item.types) {
|
|
1853
|
+
if (type.startsWith("text/")) {
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
try {
|
|
1857
|
+
const blob = await item.getType(type);
|
|
1858
|
+
const buffer = await blob.arrayBuffer();
|
|
1859
|
+
files.push({
|
|
1860
|
+
name: `clipboard-file-${files.length + 1}`,
|
|
1861
|
+
type,
|
|
1862
|
+
lastModified: Date.now(),
|
|
1863
|
+
data: arrayBufferToBase64(buffer),
|
|
1864
|
+
});
|
|
1865
|
+
} catch (error) {
|
|
1866
|
+
console.warn("[BVTRecorder] Failed to serialize clipboard blob", { type, error });
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
console.warn("[BVTRecorder] navigator.clipboard.read failed", error);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
if (!result.text && typeof navigator.clipboard.readText === "function") {
|
|
1876
|
+
try {
|
|
1877
|
+
const text = await navigator.clipboard.readText();
|
|
1878
|
+
if (text) {
|
|
1879
|
+
result.text = text;
|
|
1880
|
+
}
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
console.warn("[BVTRecorder] navigator.clipboard.readText failed", error);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
if (!result.text) {
|
|
1887
|
+
const selection = window.getSelection?.()?.toString?.();
|
|
1888
|
+
if (selection) {
|
|
1889
|
+
result.text = selection;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
if (files.length > 0) {
|
|
1894
|
+
result.files = files;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
return result;
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
return payload;
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
this.logger.error("[BVTRecorder] Error collecting clipboard payload", error);
|
|
1903
|
+
return null;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
async readClipboardPayload(message) {
|
|
1908
|
+
try {
|
|
1909
|
+
let payload = null;
|
|
1910
|
+
if (this.browserEmitter && typeof this.browserEmitter.readClipboardPayload === "function") {
|
|
1911
|
+
payload = await this.browserEmitter.readClipboardPayload();
|
|
1912
|
+
} else {
|
|
1913
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
1914
|
+
payload = await this.collectClipboardFromPage(activePage);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
if (this.hasClipboardPayload(payload)) {
|
|
1918
|
+
this.logger.info("[BVTRecorder] Remote clipboard payload ready", {
|
|
1919
|
+
hasText: !!payload.text,
|
|
1920
|
+
hasHtml: !!payload.html,
|
|
1921
|
+
files: payload.files?.length ?? 0,
|
|
1922
|
+
});
|
|
1923
|
+
this.sendEvent(this.events.clipboardPush, {
|
|
1924
|
+
data: payload,
|
|
1925
|
+
trigger: message?.trigger ?? "copy",
|
|
1926
|
+
origin: message?.source ?? "browserUI",
|
|
1927
|
+
});
|
|
1928
|
+
return payload;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
this.logger.warn("[BVTRecorder] Remote clipboard payload empty or unavailable");
|
|
1932
|
+
this.sendEvent(this.events.clipboardError, {
|
|
1933
|
+
message: "Remote clipboard is empty",
|
|
1934
|
+
trigger: message?.trigger ?? "copy",
|
|
1935
|
+
});
|
|
1936
|
+
return null;
|
|
1937
|
+
} catch (error) {
|
|
1938
|
+
this.logger.error("[BVTRecorder] Error reading clipboard payload", error);
|
|
1939
|
+
this.sendEvent(this.events.clipboardError, {
|
|
1940
|
+
message: "Failed to read clipboard contents from the remote session",
|
|
1941
|
+
trigger: message?.trigger ?? "copy",
|
|
1942
|
+
details: error instanceof Error ? error.message : String(error),
|
|
1943
|
+
});
|
|
1944
|
+
throw error;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
async injectClipboardIntoPage(page, payload) {
|
|
1949
|
+
if (!page) {
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
try {
|
|
1954
|
+
await page
|
|
1955
|
+
.context()
|
|
1956
|
+
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
1957
|
+
.catch(() => { });
|
|
1958
|
+
await page.evaluate(async (clipboardPayload) => {
|
|
1959
|
+
const toArrayBuffer = (base64) => {
|
|
1960
|
+
if (!base64) {
|
|
1961
|
+
return null;
|
|
1962
|
+
}
|
|
1963
|
+
const binaryString = atob(base64);
|
|
1964
|
+
const len = binaryString.length;
|
|
1965
|
+
const bytes = new Uint8Array(len);
|
|
1966
|
+
for (let i = 0; i < len; i += 1) {
|
|
1967
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
1968
|
+
}
|
|
1969
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
1970
|
+
};
|
|
1971
|
+
|
|
1972
|
+
const createFileFromPayload = (filePayload) => {
|
|
1973
|
+
const buffer = toArrayBuffer(filePayload?.data);
|
|
1974
|
+
if (!buffer) {
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
const name = filePayload?.name || "clipboard-file";
|
|
1978
|
+
const type = filePayload?.type || "application/octet-stream";
|
|
1979
|
+
const lastModified = filePayload?.lastModified ?? Date.now();
|
|
1980
|
+
try {
|
|
1981
|
+
return new File([buffer], name, { type, lastModified });
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
console.warn("Clipboard bridge could not recreate File object", error);
|
|
1984
|
+
return null;
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
|
|
1988
|
+
let dataTransfer = null;
|
|
1989
|
+
try {
|
|
1990
|
+
dataTransfer = new DataTransfer();
|
|
1991
|
+
} catch (error) {
|
|
1992
|
+
console.warn("Clipboard bridge could not create DataTransfer", error);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
if (dataTransfer) {
|
|
1996
|
+
if (clipboardPayload?.text) {
|
|
1997
|
+
try {
|
|
1998
|
+
dataTransfer.setData("text/plain", clipboardPayload.text);
|
|
1999
|
+
} catch (error) {
|
|
2000
|
+
console.warn("Clipboard bridge failed to set text/plain", error);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
if (clipboardPayload?.html) {
|
|
2004
|
+
try {
|
|
2005
|
+
dataTransfer.setData("text/html", clipboardPayload.html);
|
|
2006
|
+
} catch (error) {
|
|
2007
|
+
console.warn("Clipboard bridge failed to set text/html", error);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
if (Array.isArray(clipboardPayload?.files)) {
|
|
2011
|
+
for (const filePayload of clipboardPayload.files) {
|
|
2012
|
+
const file = createFileFromPayload(filePayload);
|
|
2013
|
+
if (file) {
|
|
2014
|
+
try {
|
|
2015
|
+
dataTransfer.items.add(file);
|
|
2016
|
+
} catch (error) {
|
|
2017
|
+
console.warn("Clipboard bridge failed to append file", error);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
let target = document.activeElement || document.body;
|
|
2025
|
+
if (!target) {
|
|
2026
|
+
target = document.body || null;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
let pasteHandled = false;
|
|
2030
|
+
if (dataTransfer && target && typeof target.dispatchEvent === "function") {
|
|
2031
|
+
try {
|
|
2032
|
+
const clipboardEvent = new ClipboardEvent("paste", {
|
|
2033
|
+
clipboardData: dataTransfer,
|
|
2034
|
+
bubbles: true,
|
|
2035
|
+
cancelable: true,
|
|
2036
|
+
});
|
|
2037
|
+
pasteHandled = target.dispatchEvent(clipboardEvent);
|
|
2038
|
+
} catch (error) {
|
|
2039
|
+
console.warn("Clipboard bridge failed to dispatch synthetic paste event", error);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
if (pasteHandled) {
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
const callLegacyExecCommand = (command, value) => {
|
|
2048
|
+
const execCommand = document && document["execCommand"];
|
|
2049
|
+
if (typeof execCommand === "function") {
|
|
2050
|
+
try {
|
|
2051
|
+
return execCommand.call(document, command, false, value);
|
|
2052
|
+
} catch (error) {
|
|
2053
|
+
console.warn("Clipboard bridge failed to execute legacy command", error);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return false;
|
|
2057
|
+
};
|
|
2058
|
+
|
|
2059
|
+
if (clipboardPayload?.html) {
|
|
2060
|
+
const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
|
|
2061
|
+
if (inserted) {
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
try {
|
|
2065
|
+
const selection = window.getSelection?.();
|
|
2066
|
+
if (selection && selection.rangeCount > 0) {
|
|
2067
|
+
const range = selection.getRangeAt(0);
|
|
2068
|
+
range.deleteContents();
|
|
2069
|
+
const fragment = range.createContextualFragment(clipboardPayload.html);
|
|
2070
|
+
range.insertNode(fragment);
|
|
2071
|
+
range.collapse(false);
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
} catch (error) {
|
|
2075
|
+
console.warn("Clipboard bridge could not insert HTML via Range APIs", error);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
if (clipboardPayload?.text) {
|
|
2080
|
+
const inserted = callLegacyExecCommand("insertText", clipboardPayload.text);
|
|
2081
|
+
if (inserted) {
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
try {
|
|
2085
|
+
const selection = window.getSelection?.();
|
|
2086
|
+
if (selection && selection.rangeCount > 0) {
|
|
2087
|
+
const range = selection.getRangeAt(0);
|
|
2088
|
+
range.deleteContents();
|
|
2089
|
+
range.insertNode(document.createTextNode(clipboardPayload.text));
|
|
2090
|
+
range.collapse(false);
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
} catch (error) {
|
|
2094
|
+
console.warn("Clipboard bridge could not insert text via Range APIs", error);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
if (clipboardPayload?.text && target && "value" in target) {
|
|
2099
|
+
try {
|
|
2100
|
+
const input = target;
|
|
2101
|
+
const start = input.selectionStart ?? input.value.length ?? 0;
|
|
2102
|
+
const end = input.selectionEnd ?? input.value.length ?? 0;
|
|
2103
|
+
const value = input.value ?? "";
|
|
2104
|
+
const text = clipboardPayload.text;
|
|
2105
|
+
input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
|
|
2106
|
+
const caret = start + text.length;
|
|
2107
|
+
if (typeof input.setSelectionRange === "function") {
|
|
2108
|
+
input.setSelectionRange(caret, caret);
|
|
2109
|
+
}
|
|
2110
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
console.warn("Clipboard bridge failed to mutate input element", error);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}, payload);
|
|
2116
|
+
} catch (error) {
|
|
2117
|
+
throw error;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
async createTab(url) {
|
|
2122
|
+
try {
|
|
2123
|
+
await this.browserEmitter?.createTab(url);
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
this.logger.error("Error creating tab:", error);
|
|
2126
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2127
|
+
message: "Error creating tab",
|
|
2128
|
+
code: "CREATE_TAB_ERROR",
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
async closeTab(pageId) {
|
|
2134
|
+
try {
|
|
2135
|
+
await this.browserEmitter?.closeTab(pageId);
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
this.logger.error("Error closing tab:", error);
|
|
2138
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2139
|
+
message: "Error closing tab",
|
|
2140
|
+
code: "CLOSE_TAB_ERROR",
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
async selectTab(pageId) {
|
|
2146
|
+
try {
|
|
2147
|
+
await this.browserEmitter?.selectTab(pageId);
|
|
2148
|
+
} catch (error) {
|
|
2149
|
+
this.logger.error("Error selecting tab:", error);
|
|
2150
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2151
|
+
message: "Error selecting tab",
|
|
2152
|
+
code: "SELECT_TAB_ERROR",
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
async navigateTab({ pageId, url }) {
|
|
2158
|
+
try {
|
|
2159
|
+
if (!pageId || !url) {
|
|
2160
|
+
this.logger.error("navigateTab called without pageId or url", { pageId, url });
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
await this.browserEmitter?.navigateTab(pageId, url);
|
|
2164
|
+
} catch (error) {
|
|
2165
|
+
this.logger.error("Error navigating tab:", error);
|
|
2166
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2167
|
+
message: "Error navigating tab",
|
|
2168
|
+
code: "NAVIGATE_TAB_ERROR",
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
async reloadTab(pageId) {
|
|
2174
|
+
try {
|
|
2175
|
+
await this.browserEmitter?.reloadTab(pageId);
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
this.logger.error("Error reloading tab:", error);
|
|
2178
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2179
|
+
message: "Error reloading tab",
|
|
2180
|
+
code: "RELOAD_TAB_ERROR",
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
async goBack(pageId) {
|
|
2186
|
+
try {
|
|
2187
|
+
await this.browserEmitter?.goBack(pageId);
|
|
2188
|
+
} catch (error) {
|
|
2189
|
+
this.logger.error("Error navigating back:", error);
|
|
2190
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2191
|
+
message: "Error navigating back",
|
|
2192
|
+
code: "GO_BACK_ERROR",
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
async goForward(pageId) {
|
|
2198
|
+
try {
|
|
2199
|
+
await this.browserEmitter?.goForward(pageId);
|
|
2200
|
+
} catch (error) {
|
|
2201
|
+
this.logger.error("Error navigating forward:", error);
|
|
2202
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2203
|
+
message: "Error navigating forward",
|
|
2204
|
+
code: "GO_FORWARD_ERROR",
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
1238
2208
|
}
|