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