@dev-blinq/cucumber_client 1.0.1403-dev → 1.0.1403-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 +105 -105
- 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 +163 -44
- 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 +5 -1
- package/bin/client/code_gen/api_codegen.js +2 -2
- package/bin/client/code_gen/code_inversion.js +63 -2
- package/bin/client/code_gen/function_signature.js +4 -0
- package/bin/client/code_gen/page_reflection.js +846 -906
- package/bin/client/code_gen/playwright_codeget.js +27 -3
- package/bin/client/cucumber/feature.js +4 -0
- 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 +6 -3
- package/bin/client/cucumber_selector.js +17 -1
- package/bin/client/local_agent.js +3 -2
- package/bin/client/parse_feature_file.js +23 -26
- package/bin/client/playground/projects/env.json +2 -2
- package/bin/client/project.js +186 -202
- package/bin/client/recorderv3/bvt_init.js +349 -0
- package/bin/client/recorderv3/bvt_recorder.js +1069 -104
- package/bin/client/recorderv3/implemented_steps.js +2 -0
- package/bin/client/recorderv3/index.js +4 -303
- package/bin/client/recorderv3/scriptTest.js +1 -1
- package/bin/client/recorderv3/services.js +814 -154
- package/bin/client/recorderv3/step_runner.js +315 -206
- package/bin/client/recorderv3/step_utils.js +499 -37
- package/bin/client/recorderv3/update_feature.js +9 -5
- package/bin/client/recorderv3/wbr_entry.js +61 -0
- package/bin/client/recording.js +1 -0
- package/bin/client/upload-service.js +3 -2
- package/bin/client/utils/socket_logger.js +132 -0
- package/bin/index.js +4 -1
- package/bin/logger.js +3 -2
- package/bin/min/consoleApi.min.cjs +2 -3
- package/bin/min/injectedScript.min.cjs +16 -16
- package/package.json +19 -9
|
@@ -4,22 +4,330 @@ import { existsSync, readdirSync, readFileSync, rmSync } from "fs";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import url from "url";
|
|
6
6
|
import { getImplementedSteps, parseRouteFiles } from "./implemented_steps.js";
|
|
7
|
-
import { NamesService } from "./services.js";
|
|
7
|
+
import { NamesService, RemoteBrowserService, PublishService } from "./services.js";
|
|
8
8
|
import { BVTStepRunner } from "./step_runner.js";
|
|
9
9
|
import { readFile, writeFile } from "fs/promises";
|
|
10
|
-
import {
|
|
11
|
-
import { updateFeatureFile } from "./update_feature.js";
|
|
10
|
+
import { loadStepDefinitions, getCommandsForImplementedStep } from "./step_utils.js";
|
|
12
11
|
import { parseStepTextParameters } from "../cucumber/utils.js";
|
|
13
12
|
import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
|
|
14
13
|
import chokidar from "chokidar";
|
|
15
|
-
import logger from "../../logger.js";
|
|
16
14
|
import { unEscapeNonPrintables } from "../cucumber/utils.js";
|
|
17
15
|
import { findAvailablePort } from "../utils/index.js";
|
|
18
|
-
import
|
|
16
|
+
import socketLogger from "../utils/socket_logger.js";
|
|
17
|
+
import { tmpdir } from "os";
|
|
18
|
+
import { faker } from "@faker-js/faker/locale/en_US";
|
|
19
|
+
import { chromium } from "playwright-core";
|
|
19
20
|
|
|
20
21
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
21
22
|
|
|
22
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
|
+
|
|
23
331
|
export function getInitScript(config, options) {
|
|
24
332
|
const preScript = `
|
|
25
333
|
window.__bvt_Recorder_config = ${JSON.stringify(config ?? null)};
|
|
@@ -29,7 +337,7 @@ export function getInitScript(config, options) {
|
|
|
29
337
|
path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"),
|
|
30
338
|
"utf8"
|
|
31
339
|
);
|
|
32
|
-
return preScript + recorderScript;
|
|
340
|
+
return preScript + recorderScript + clipboardBridgeScript;
|
|
33
341
|
}
|
|
34
342
|
|
|
35
343
|
async function evaluate(frame, script) {
|
|
@@ -45,7 +353,6 @@ async function evaluate(frame, script) {
|
|
|
45
353
|
async function findNestedFrameSelector(frame, obj) {
|
|
46
354
|
try {
|
|
47
355
|
const parent = frame.parentFrame();
|
|
48
|
-
if (parent) console.log(`Parent frame: ${JSON.stringify(parent)}`);
|
|
49
356
|
if (!parent) return { children: obj };
|
|
50
357
|
const frameElement = await frame.frameElement();
|
|
51
358
|
if (!frameElement) return;
|
|
@@ -54,6 +361,7 @@ async function findNestedFrameSelector(frame, obj) {
|
|
|
54
361
|
}, frameElement);
|
|
55
362
|
return findNestedFrameSelector(parent, { children: obj, selectors });
|
|
56
363
|
} catch (e) {
|
|
364
|
+
socketLogger.error(`Error in findNestedFrameSelector: ${e}`);
|
|
57
365
|
console.error(e);
|
|
58
366
|
}
|
|
59
367
|
}
|
|
@@ -150,17 +458,30 @@ const transformAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode,
|
|
|
150
458
|
};
|
|
151
459
|
}
|
|
152
460
|
default: {
|
|
461
|
+
socketLogger.error(`Action not supported: ${action.name}`);
|
|
153
462
|
console.log("action not supported", action);
|
|
154
463
|
throw new Error("action not supported");
|
|
155
464
|
}
|
|
156
465
|
}
|
|
157
466
|
};
|
|
467
|
+
const diffPaths = (currentPath, newPath) => {
|
|
468
|
+
const currentDomain = new URL(currentPath).hostname;
|
|
469
|
+
const newDomain = new URL(newPath).hostname;
|
|
470
|
+
if (currentDomain !== newDomain) {
|
|
471
|
+
return true;
|
|
472
|
+
} else {
|
|
473
|
+
const currentRoute = new URL(currentPath).pathname;
|
|
474
|
+
const newRoute = new URL(newPath).pathname;
|
|
475
|
+
return currentRoute !== newRoute;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
158
478
|
/**
|
|
159
479
|
* @typedef {Object} BVTRecorderInput
|
|
160
480
|
* @property {string} envName
|
|
161
481
|
* @property {string} projectDir
|
|
162
482
|
* @property {string} TOKEN
|
|
163
483
|
* @property {(name:string, data:any)=> void} sendEvent
|
|
484
|
+
* @property {Object} logger
|
|
164
485
|
*/
|
|
165
486
|
export class BVTRecorder {
|
|
166
487
|
#currentURL = "";
|
|
@@ -174,7 +495,6 @@ export class BVTRecorder {
|
|
|
174
495
|
*/
|
|
175
496
|
constructor(initialState) {
|
|
176
497
|
Object.assign(this, initialState);
|
|
177
|
-
this.logger = logger;
|
|
178
498
|
this.screenshotMap = new Map();
|
|
179
499
|
this.snapshotMap = new Map();
|
|
180
500
|
this.scenariosStepsMap = new Map();
|
|
@@ -184,40 +504,19 @@ export class BVTRecorder {
|
|
|
184
504
|
projectDir: this.projectDir,
|
|
185
505
|
logger: this.logger,
|
|
186
506
|
});
|
|
187
|
-
this.
|
|
188
|
-
projectDir: this.projectDir,
|
|
189
|
-
sendExecutionStatus: (data) => {
|
|
190
|
-
if (data && data.type) {
|
|
191
|
-
switch (data.type) {
|
|
192
|
-
case "cmdExecutionStart":
|
|
193
|
-
console.log("Sending cmdExecutionStart event for cmdId:", data);
|
|
194
|
-
this.sendEvent(this.events.cmdExecutionStart, data);
|
|
195
|
-
break;
|
|
196
|
-
case "cmdExecutionSuccess":
|
|
197
|
-
console.log("Sending cmdExecutionSuccess event for cmdId:", data);
|
|
198
|
-
this.sendEvent(this.events.cmdExecutionSuccess, data);
|
|
199
|
-
break;
|
|
200
|
-
case "cmdExecutionError":
|
|
201
|
-
console.log("Sending cmdExecutionError event for cmdId:", data);
|
|
202
|
-
this.sendEvent(this.events.cmdExecutionError, data);
|
|
203
|
-
break;
|
|
204
|
-
case "interceptResults":
|
|
205
|
-
console.log("Sending interceptResults event");
|
|
206
|
-
this.sendEvent(this.events.interceptResults, data);
|
|
207
|
-
break;
|
|
208
|
-
default:
|
|
209
|
-
console.warn("Unknown command execution status type:", data.type);
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
});
|
|
507
|
+
this.workspaceService = new PublishService(this.TOKEN);
|
|
215
508
|
this.pageSet = new Set();
|
|
216
509
|
this.lastKnownUrlPath = "";
|
|
217
|
-
// TODO: what is world?
|
|
218
510
|
this.world = { attach: () => {} };
|
|
219
511
|
this.shouldTakeScreenshot = true;
|
|
220
512
|
this.watcher = null;
|
|
513
|
+
this.networkEventsFolder = path.join(tmpdir(), "blinq_network_events");
|
|
514
|
+
this.tempProjectFolder = `${tmpdir()}/bvt_temp_project_${Math.floor(Math.random() * 1000000)}`;
|
|
515
|
+
this.tempSnapshotsFolder = path.join(this.tempProjectFolder, "data/snapshots");
|
|
516
|
+
|
|
517
|
+
if (existsSync(this.networkEventsFolder)) {
|
|
518
|
+
rmSync(this.networkEventsFolder, { recursive: true, force: true });
|
|
519
|
+
}
|
|
221
520
|
}
|
|
222
521
|
events = {
|
|
223
522
|
onFrameNavigate: "BVTRecorder.onFrameNavigate",
|
|
@@ -232,12 +531,18 @@ export class BVTRecorder {
|
|
|
232
531
|
cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
|
|
233
532
|
cmdExecutionError: "BVTRecorder.cmdExecutionError",
|
|
234
533
|
interceptResults: "BVTRecorder.interceptResults",
|
|
534
|
+
onDebugURLChange: "BVTRecorder.onDebugURLChange",
|
|
535
|
+
updateCommand: "BVTRecorder.updateCommand",
|
|
536
|
+
browserStateSync: "BrowserService.stateSync",
|
|
537
|
+
browserStateError: "BrowserService.stateError",
|
|
538
|
+
clipboardPush: "BrowserService.clipboardPush",
|
|
539
|
+
clipboardError: "BrowserService.clipboardError",
|
|
235
540
|
};
|
|
236
541
|
bindings = {
|
|
237
542
|
__bvt_recordCommand: async ({ frame, page, context }, event) => {
|
|
238
543
|
this.#activeFrame = frame;
|
|
239
544
|
const nestFrmLoc = await findNestedFrameSelector(frame);
|
|
240
|
-
|
|
545
|
+
this.logger.info(`Time taken for action: ${event.statistics.time}`);
|
|
241
546
|
await this.onAction({ ...event, nestFrmLoc });
|
|
242
547
|
},
|
|
243
548
|
__bvt_getMode: async () => {
|
|
@@ -256,12 +561,30 @@ export class BVTRecorder {
|
|
|
256
561
|
await this.onClosePopup();
|
|
257
562
|
},
|
|
258
563
|
__bvt_log: async (src, message) => {
|
|
259
|
-
|
|
260
|
-
console.log(`Inside Browser: ${message}`);
|
|
564
|
+
this.logger.info(`Inside Browser: ${message}`);
|
|
261
565
|
},
|
|
262
566
|
__bvt_getObject: (_src, obj) => {
|
|
263
567
|
this.processObject(obj);
|
|
264
568
|
},
|
|
569
|
+
__bvt_reportClipboard: async ({ page }, payload) => {
|
|
570
|
+
try {
|
|
571
|
+
if (!payload) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
575
|
+
if (activePage && activePage !== page) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const pageUrl = typeof page?.url === "function" ? page.url() : null;
|
|
579
|
+
this.sendEvent(this.events.clipboardPush, {
|
|
580
|
+
data: payload,
|
|
581
|
+
trigger: payload?.trigger ?? "copy",
|
|
582
|
+
pageUrl,
|
|
583
|
+
});
|
|
584
|
+
} catch (error) {
|
|
585
|
+
this.logger.error("Error forwarding clipboard payload from page", error);
|
|
586
|
+
}
|
|
587
|
+
},
|
|
265
588
|
};
|
|
266
589
|
|
|
267
590
|
getSnapshot = async (attr) => {
|
|
@@ -319,10 +642,14 @@ export class BVTRecorder {
|
|
|
319
642
|
}
|
|
320
643
|
|
|
321
644
|
async _initBrowser({ url }) {
|
|
322
|
-
|
|
323
|
-
|
|
645
|
+
if (process.env.CDP_LISTEN_PORT === undefined) {
|
|
646
|
+
this.#remoteDebuggerPort = await findAvailablePort();
|
|
647
|
+
process.env.CDP_LISTEN_PORT = this.#remoteDebuggerPort;
|
|
648
|
+
} else {
|
|
649
|
+
this.#remoteDebuggerPort = process.env.CDP_LISTEN_PORT;
|
|
650
|
+
}
|
|
324
651
|
|
|
325
|
-
this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
|
|
652
|
+
// this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
|
|
326
653
|
this.world = { attach: () => {} };
|
|
327
654
|
|
|
328
655
|
const ai_config_file = path.join(this.projectDir, "ai_config.json");
|
|
@@ -331,7 +658,7 @@ export class BVTRecorder {
|
|
|
331
658
|
try {
|
|
332
659
|
ai_config = JSON.parse(readFileSync(ai_config_file, "utf8"));
|
|
333
660
|
} catch (error) {
|
|
334
|
-
|
|
661
|
+
this.logger.error("Error reading ai_config.json", error);
|
|
335
662
|
}
|
|
336
663
|
}
|
|
337
664
|
this.config = ai_config;
|
|
@@ -343,18 +670,48 @@ export class BVTRecorder {
|
|
|
343
670
|
],
|
|
344
671
|
};
|
|
345
672
|
|
|
346
|
-
let startTime = Date.now();
|
|
347
673
|
const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName);
|
|
348
|
-
let stopTime = Date.now();
|
|
349
|
-
this.logger.info(`Browser launched in ${(stopTime - startTime) / 1000} s`);
|
|
350
674
|
this.bvtContext = bvtContext;
|
|
351
|
-
|
|
352
|
-
|
|
675
|
+
this.stepRunner = new BVTStepRunner({
|
|
676
|
+
projectDir: this.projectDir,
|
|
677
|
+
sendExecutionStatus: (data) => {
|
|
678
|
+
if (data && data.type) {
|
|
679
|
+
switch (data.type) {
|
|
680
|
+
case "cmdExecutionStart":
|
|
681
|
+
this.sendEvent(this.events.cmdExecutionStart, data);
|
|
682
|
+
break;
|
|
683
|
+
case "cmdExecutionSuccess":
|
|
684
|
+
this.sendEvent(this.events.cmdExecutionSuccess, data);
|
|
685
|
+
break;
|
|
686
|
+
case "cmdExecutionError":
|
|
687
|
+
this.sendEvent(this.events.cmdExecutionError, data);
|
|
688
|
+
break;
|
|
689
|
+
case "interceptResults":
|
|
690
|
+
this.sendEvent(this.events.interceptResults, data);
|
|
691
|
+
break;
|
|
692
|
+
default:
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
bvtContext: this.bvtContext,
|
|
698
|
+
});
|
|
699
|
+
this.context = bvtContext.playContext;
|
|
353
700
|
this.web = bvtContext.stable || bvtContext.web;
|
|
354
701
|
this.web.tryAllStrategies = true;
|
|
355
702
|
this.page = bvtContext.page;
|
|
356
|
-
|
|
357
703
|
this.pageSet.add(this.page);
|
|
704
|
+
if (process.env.REMOTE_RECORDER === "true") {
|
|
705
|
+
this.browserEmitter = new RemoteBrowserService({
|
|
706
|
+
CDP_CONNECT_URL: `http://localhost:${this.#remoteDebuggerPort}`,
|
|
707
|
+
context: this.context,
|
|
708
|
+
});
|
|
709
|
+
this.browserEmitter.on(this.events.browserStateSync, (state) => {
|
|
710
|
+
this.page = this.browserEmitter.getSelectedPage();
|
|
711
|
+
this.sendEvent(this.events.browserStateSync, state);
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
358
715
|
this.lastKnownUrlPath = this._updateUrlPath();
|
|
359
716
|
const browser = await this.context.browser();
|
|
360
717
|
this.browser = browser;
|
|
@@ -367,6 +724,14 @@ export class BVTRecorder {
|
|
|
367
724
|
this.web.onRestoreSaveState = (url) => {
|
|
368
725
|
this._initBrowser({ url });
|
|
369
726
|
};
|
|
727
|
+
|
|
728
|
+
// create a second browser for locator generation
|
|
729
|
+
this.backgroundBrowser = await chromium.launch({
|
|
730
|
+
headless: true,
|
|
731
|
+
});
|
|
732
|
+
this.backgroundContext = await this.backgroundBrowser.newContext({});
|
|
733
|
+
await this.backgroundContext.addInitScript({ content: this.getInitScripts(this.config) });
|
|
734
|
+
await this.backgroundContext.newPage();
|
|
370
735
|
}
|
|
371
736
|
async onClosePopup() {
|
|
372
737
|
// console.log("close popups");
|
|
@@ -381,13 +746,15 @@ export class BVTRecorder {
|
|
|
381
746
|
}
|
|
382
747
|
return;
|
|
383
748
|
} catch (error) {
|
|
384
|
-
console.error("Error evaluting in context:", error);
|
|
749
|
+
// console.error("Error evaluting in context:", error);
|
|
750
|
+
this.logger.error("Error evaluating in context:", error);
|
|
385
751
|
}
|
|
386
752
|
}
|
|
387
753
|
}
|
|
388
754
|
|
|
389
755
|
getMode() {
|
|
390
|
-
console.log("getMode", this.#mode);
|
|
756
|
+
// console.log("getMode", this.#mode);
|
|
757
|
+
this.logger.info("Current mode:", this.#mode);
|
|
391
758
|
return this.#mode;
|
|
392
759
|
}
|
|
393
760
|
|
|
@@ -406,13 +773,14 @@ export class BVTRecorder {
|
|
|
406
773
|
|
|
407
774
|
await this.page.goto(url, {
|
|
408
775
|
waitUntil: "domcontentloaded",
|
|
776
|
+
timeout: this.config.page_timeout ?? 60_000,
|
|
409
777
|
});
|
|
410
778
|
// add listener for frame navigation on current tab
|
|
411
779
|
this._addFrameNavigateListener(this.page);
|
|
412
780
|
|
|
413
781
|
// eval init script on current tab
|
|
414
782
|
// await this._initPage(this.page);
|
|
415
|
-
this.#currentURL =
|
|
783
|
+
this.#currentURL = url;
|
|
416
784
|
|
|
417
785
|
await this.page.dispatchEvent("html", "scroll");
|
|
418
786
|
await delay(1000);
|
|
@@ -429,6 +797,8 @@ export class BVTRecorder {
|
|
|
429
797
|
this.sendEvent(this.events.onBrowserClose);
|
|
430
798
|
}
|
|
431
799
|
} catch (error) {
|
|
800
|
+
this.logger.error("Error in page close event");
|
|
801
|
+
this.logger.error(error);
|
|
432
802
|
console.error("Error in page close event");
|
|
433
803
|
console.error(error);
|
|
434
804
|
}
|
|
@@ -439,8 +809,10 @@ export class BVTRecorder {
|
|
|
439
809
|
if (frame !== page.mainFrame()) return;
|
|
440
810
|
this.handlePageTransition();
|
|
441
811
|
} catch (error) {
|
|
812
|
+
this.logger.error("Error in handlePageTransition event");
|
|
813
|
+
this.logger.error(error);
|
|
442
814
|
console.error("Error in handlePageTransition event");
|
|
443
|
-
|
|
815
|
+
console.error(error);
|
|
444
816
|
}
|
|
445
817
|
try {
|
|
446
818
|
if (frame !== this.#activeFrame) return;
|
|
@@ -450,15 +822,18 @@ export class BVTRecorder {
|
|
|
450
822
|
element: { inputID: "frame" },
|
|
451
823
|
});
|
|
452
824
|
|
|
453
|
-
const
|
|
825
|
+
const newUrl = frame.url();
|
|
826
|
+
const newPath = new URL(newUrl).pathname;
|
|
454
827
|
const newTitle = await frame.title();
|
|
455
|
-
|
|
828
|
+
const changed = diffPaths(this.#currentURL, newUrl);
|
|
829
|
+
|
|
830
|
+
if (changed) {
|
|
456
831
|
this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
|
|
457
|
-
this.#currentURL =
|
|
832
|
+
this.#currentURL = newUrl;
|
|
458
833
|
}
|
|
459
|
-
// await this._setRecordingMode(frame);
|
|
460
|
-
// await this._initPage(page);
|
|
461
834
|
} catch (error) {
|
|
835
|
+
this.logger.error("Error in frame navigate event");
|
|
836
|
+
this.logger.error(error);
|
|
462
837
|
console.error("Error in frame navigate event");
|
|
463
838
|
// console.error(error);
|
|
464
839
|
}
|
|
@@ -541,13 +916,9 @@ export class BVTRecorder {
|
|
|
541
916
|
|
|
542
917
|
try {
|
|
543
918
|
const result = await client.send("Page.getNavigationHistory");
|
|
544
|
-
// console.log("Navigation History:", result);
|
|
545
919
|
const entries = result.entries;
|
|
546
920
|
const currentIndex = result.currentIndex;
|
|
547
921
|
|
|
548
|
-
// ignore if currentIndex is not the last entry
|
|
549
|
-
// if (currentIndex !== entries.length - 1) return;
|
|
550
|
-
|
|
551
922
|
const currentEntry = entries[currentIndex];
|
|
552
923
|
const transitionInfo = this.analyzeTransitionType(entries, currentIndex, currentEntry);
|
|
553
924
|
this.previousIndex = currentIndex;
|
|
@@ -560,6 +931,8 @@ export class BVTRecorder {
|
|
|
560
931
|
navigationAction: transitionInfo.action,
|
|
561
932
|
};
|
|
562
933
|
} catch (error) {
|
|
934
|
+
this.logger.error("Error in getCurrentTransition event");
|
|
935
|
+
this.logger.error(error);
|
|
563
936
|
console.error("Error in getTransistionType event", error);
|
|
564
937
|
} finally {
|
|
565
938
|
await client.detach();
|
|
@@ -622,12 +995,13 @@ export class BVTRecorder {
|
|
|
622
995
|
try {
|
|
623
996
|
if (page.isClosed()) return;
|
|
624
997
|
this.pageSet.add(page);
|
|
625
|
-
|
|
626
998
|
await page.waitForLoadState("domcontentloaded");
|
|
627
999
|
|
|
628
1000
|
// add listener for frame navigation on new tab
|
|
629
1001
|
this._addFrameNavigateListener(page);
|
|
630
1002
|
} catch (error) {
|
|
1003
|
+
this.logger.error("Error in page event");
|
|
1004
|
+
this.logger.error(error);
|
|
631
1005
|
console.error("Error in page event");
|
|
632
1006
|
console.error(error);
|
|
633
1007
|
}
|
|
@@ -669,6 +1043,7 @@ export class BVTRecorder {
|
|
|
669
1043
|
const { data } = await client.send("Page.captureScreenshot", { format: "png" });
|
|
670
1044
|
return data;
|
|
671
1045
|
} catch (error) {
|
|
1046
|
+
this.logger.error("Error in taking browser screenshot");
|
|
672
1047
|
console.error("Error in taking browser screenshot", error);
|
|
673
1048
|
} finally {
|
|
674
1049
|
await client.detach();
|
|
@@ -684,6 +1059,52 @@ export class BVTRecorder {
|
|
|
684
1059
|
console.error("Error in saving screenshot: ", error);
|
|
685
1060
|
}
|
|
686
1061
|
}
|
|
1062
|
+
async generateLocators(event) {
|
|
1063
|
+
const snapshotDetails = event.snapshotDetails;
|
|
1064
|
+
if (!snapshotDetails) {
|
|
1065
|
+
throw new Error("No snapshot details found");
|
|
1066
|
+
}
|
|
1067
|
+
const mode = event.mode;
|
|
1068
|
+
const inputID = event.element.inputID;
|
|
1069
|
+
|
|
1070
|
+
const { id, contextId, doc } = snapshotDetails;
|
|
1071
|
+
// const selector = `[data-blinq-id="${id}"]`;
|
|
1072
|
+
const newPage = await this.backgroundContext.newPage();
|
|
1073
|
+
await newPage.setContent(doc, { waitUntil: "domcontentloaded" });
|
|
1074
|
+
const locatorsObj = await newPage.evaluate(
|
|
1075
|
+
([id, contextId, mode]) => {
|
|
1076
|
+
const recorder = window.__bvt_Recorder;
|
|
1077
|
+
const contextElement = document.querySelector(`[data-blinq-context-id="${contextId}"]`);
|
|
1078
|
+
const el = document.querySelector(`[data-blinq-id="${id}"]`);
|
|
1079
|
+
if (contextElement) {
|
|
1080
|
+
const result = recorder.locatorGenerator.toContextLocators(el, contextElement);
|
|
1081
|
+
return result;
|
|
1082
|
+
}
|
|
1083
|
+
const isRecordingText = mode === "recordingText";
|
|
1084
|
+
return recorder.locatorGenerator.getElementLocators(el, {
|
|
1085
|
+
excludeText: isRecordingText,
|
|
1086
|
+
});
|
|
1087
|
+
},
|
|
1088
|
+
[id, contextId, mode]
|
|
1089
|
+
);
|
|
1090
|
+
|
|
1091
|
+
// console.log(`Generated locators: for ${inputID}: `, JSON.stringify(locatorsObj));
|
|
1092
|
+
await newPage.close();
|
|
1093
|
+
if (event.nestFrmLoc?.children) {
|
|
1094
|
+
locatorsObj.nestFrmLoc = event.nestFrmLoc.children;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
this.sendEvent(this.events.updateCommand, {
|
|
1098
|
+
locators: {
|
|
1099
|
+
locators: locatorsObj.locators,
|
|
1100
|
+
nestFrmLoc: locatorsObj.nestFrmLoc,
|
|
1101
|
+
iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
1102
|
+
},
|
|
1103
|
+
allStrategyLocators: locatorsObj.allStrategyLocators,
|
|
1104
|
+
inputID,
|
|
1105
|
+
});
|
|
1106
|
+
// const
|
|
1107
|
+
}
|
|
687
1108
|
async onAction(event) {
|
|
688
1109
|
this._updateUrlPath();
|
|
689
1110
|
// const locators = this.overlayLocators(event);
|
|
@@ -697,25 +1118,26 @@ export class BVTRecorder {
|
|
|
697
1118
|
event.mode === "recordingHover",
|
|
698
1119
|
event.mode === "multiInspecting"
|
|
699
1120
|
),
|
|
700
|
-
locators: {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
},
|
|
704
|
-
allStrategyLocators: event.allStrategyLocators,
|
|
1121
|
+
// locators: {
|
|
1122
|
+
// locators: event.locators,
|
|
1123
|
+
// iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
1124
|
+
// },
|
|
1125
|
+
// allStrategyLocators: event.allStrategyLocators,
|
|
705
1126
|
url: event.frame.url,
|
|
706
1127
|
title: event.frame.title,
|
|
707
1128
|
extract: {},
|
|
708
1129
|
lastKnownUrlPath: this.lastKnownUrlPath,
|
|
709
1130
|
};
|
|
710
|
-
if (event.nestFrmLoc?.children) {
|
|
711
|
-
|
|
712
|
-
}
|
|
1131
|
+
// if (event.nestFrmLoc?.children) {
|
|
1132
|
+
// cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
|
|
1133
|
+
// }
|
|
713
1134
|
// this.logger.info({ event });
|
|
714
1135
|
if (this.shouldTakeScreenshot) {
|
|
715
1136
|
await this.storeScreenshot(event);
|
|
716
1137
|
}
|
|
717
1138
|
this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
718
1139
|
this._updateUrlPath();
|
|
1140
|
+
await this.generateLocators(event);
|
|
719
1141
|
}
|
|
720
1142
|
_updateUrlPath() {
|
|
721
1143
|
try {
|
|
@@ -737,7 +1159,6 @@ export class BVTRecorder {
|
|
|
737
1159
|
this.previousHistoryLength = null;
|
|
738
1160
|
this.previousUrl = null;
|
|
739
1161
|
this.previousEntries = null;
|
|
740
|
-
|
|
741
1162
|
await closeContext();
|
|
742
1163
|
this.pageSet.clear();
|
|
743
1164
|
}
|
|
@@ -789,7 +1210,6 @@ export class BVTRecorder {
|
|
|
789
1210
|
}
|
|
790
1211
|
|
|
791
1212
|
async startRecordingInput() {
|
|
792
|
-
console.log("startRecordingInput");
|
|
793
1213
|
await this.setMode("recordingInput");
|
|
794
1214
|
}
|
|
795
1215
|
async stopRecordingInput() {
|
|
@@ -813,9 +1233,17 @@ export class BVTRecorder {
|
|
|
813
1233
|
}
|
|
814
1234
|
|
|
815
1235
|
async abortExecution() {
|
|
816
|
-
this.bvtContext.web.abortedExecution = true;
|
|
817
1236
|
await this.stepRunner.abortExecution();
|
|
818
1237
|
}
|
|
1238
|
+
|
|
1239
|
+
async pauseExecution({ cmdId }) {
|
|
1240
|
+
await this.stepRunner.pauseExecution(cmdId);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
async resumeExecution({ cmdId }) {
|
|
1244
|
+
await this.stepRunner.resumeExecution(cmdId);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
819
1247
|
async dealyedRevertMode() {
|
|
820
1248
|
const timerId = setTimeout(async () => {
|
|
821
1249
|
await this.revertMode();
|
|
@@ -824,18 +1252,27 @@ export class BVTRecorder {
|
|
|
824
1252
|
}
|
|
825
1253
|
async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
|
|
826
1254
|
const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
|
|
1255
|
+
|
|
1256
|
+
const env = path.basename(this.envName, ".json");
|
|
827
1257
|
const _env = {
|
|
828
1258
|
TOKEN: this.TOKEN,
|
|
829
1259
|
TEMP_RUN: true,
|
|
830
1260
|
REPORT_FOLDER: this.bvtContext.reportFolder,
|
|
831
1261
|
BLINQ_ENV: this.envName,
|
|
832
|
-
|
|
833
|
-
|
|
1262
|
+
DEBUG: "blinq:route",
|
|
1263
|
+
// BVT_TEMP_SNAPSHOTS_FOLDER: step.isImplemented ? path.join(this.tempSnapshotsFolder, env) : undefined,
|
|
834
1264
|
};
|
|
1265
|
+
if (!step.isImplemented) {
|
|
1266
|
+
_env.BVT_TEMP_SNAPSHOTS_FOLDER = path.join(this.tempSnapshotsFolder, env);
|
|
1267
|
+
}
|
|
835
1268
|
|
|
836
1269
|
this.bvtContext.navigate = true;
|
|
837
1270
|
this.bvtContext.loadedRoutes = null;
|
|
838
|
-
|
|
1271
|
+
if (listenNetwork) {
|
|
1272
|
+
this.bvtContext.STORE_DETAILED_NETWORK_DATA = true;
|
|
1273
|
+
} else {
|
|
1274
|
+
this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
|
|
1275
|
+
}
|
|
839
1276
|
for (const [key, value] of Object.entries(_env)) {
|
|
840
1277
|
process.env[key] = value;
|
|
841
1278
|
}
|
|
@@ -871,13 +1308,26 @@ export class BVTRecorder {
|
|
|
871
1308
|
delete process.env[key];
|
|
872
1309
|
}
|
|
873
1310
|
this.bvtContext.navigate = false;
|
|
874
|
-
this.bvtContext.web.abortedExecution = false;
|
|
875
1311
|
}
|
|
876
1312
|
}
|
|
877
|
-
async saveScenario({ scenario, featureName, override, isSingleStep }) {
|
|
878
|
-
await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
|
|
879
|
-
if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
|
|
880
|
-
await this.
|
|
1313
|
+
async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env }) {
|
|
1314
|
+
// await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
|
|
1315
|
+
// if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
|
|
1316
|
+
const res = await this.workspaceService.saveScenario({
|
|
1317
|
+
scenario,
|
|
1318
|
+
featureName,
|
|
1319
|
+
override,
|
|
1320
|
+
isSingleStep,
|
|
1321
|
+
branch,
|
|
1322
|
+
isEditing,
|
|
1323
|
+
projectId: path.basename(this.projectDir),
|
|
1324
|
+
env: env ?? this.envName,
|
|
1325
|
+
});
|
|
1326
|
+
if (res.success) {
|
|
1327
|
+
await this.cleanup({ tags: scenario.tags });
|
|
1328
|
+
} else {
|
|
1329
|
+
throw new Error(res.message || "Error saving scenario");
|
|
1330
|
+
}
|
|
881
1331
|
}
|
|
882
1332
|
async getImplementedSteps() {
|
|
883
1333
|
const stepsAndScenarios = await getImplementedSteps(this.projectDir);
|
|
@@ -961,10 +1411,11 @@ export class BVTRecorder {
|
|
|
961
1411
|
if (existsSync(_getDataFile(this.world, this.bvtContext, this.web))) {
|
|
962
1412
|
try {
|
|
963
1413
|
const testData = JSON.parse(readFileSync(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
|
|
964
|
-
this.logger.info("Test data", testData);
|
|
1414
|
+
// this.logger.info("Test data", testData);
|
|
965
1415
|
this.sendEvent(this.events.getTestData, testData);
|
|
966
1416
|
} catch (e) {
|
|
967
|
-
this.logger.error("Error reading test data file", e);
|
|
1417
|
+
// this.logger.error("Error reading test data file", e);
|
|
1418
|
+
console.log("Error reading test data file", e);
|
|
968
1419
|
}
|
|
969
1420
|
}
|
|
970
1421
|
|
|
@@ -973,10 +1424,12 @@ export class BVTRecorder {
|
|
|
973
1424
|
this.watcher.on("all", async (event, path) => {
|
|
974
1425
|
try {
|
|
975
1426
|
const testData = JSON.parse(await readFile(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
|
|
976
|
-
this.logger.info("Test data", testData);
|
|
1427
|
+
// this.logger.info("Test data", testData);
|
|
1428
|
+
console.log("Test data changed", testData);
|
|
977
1429
|
this.sendEvent(this.events.getTestData, testData);
|
|
978
1430
|
} catch (e) {
|
|
979
|
-
this.logger.error("Error reading test data file", e);
|
|
1431
|
+
// this.logger.error("Error reading test data file", e);
|
|
1432
|
+
console.log("Error reading test data file", e);
|
|
980
1433
|
}
|
|
981
1434
|
});
|
|
982
1435
|
}
|
|
@@ -1009,7 +1462,7 @@ export class BVTRecorder {
|
|
|
1009
1462
|
.filter((file) => file.endsWith(".feature"))
|
|
1010
1463
|
.map((file) => path.join(this.projectDir, "features", file));
|
|
1011
1464
|
try {
|
|
1012
|
-
const parsedFiles = featureFiles.map((file) => parseFeatureFile(file));
|
|
1465
|
+
const parsedFiles = featureFiles.map((file) => this.parseFeatureFile(file));
|
|
1013
1466
|
const output = {};
|
|
1014
1467
|
parsedFiles.forEach((file) => {
|
|
1015
1468
|
if (!file.feature) return;
|
|
@@ -1037,7 +1490,7 @@ export class BVTRecorder {
|
|
|
1037
1490
|
loadExistingScenario({ featureName, scenarioName }) {
|
|
1038
1491
|
const step_definitions = loadStepDefinitions(this.projectDir);
|
|
1039
1492
|
const featureFilePath = path.join(this.projectDir, "features", featureName);
|
|
1040
|
-
const gherkinDoc = parseFeatureFile(featureFilePath);
|
|
1493
|
+
const gherkinDoc = this.parseFeatureFile(featureFilePath);
|
|
1041
1494
|
const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
|
|
1042
1495
|
|
|
1043
1496
|
const steps = [];
|
|
@@ -1196,20 +1649,532 @@ export class BVTRecorder {
|
|
|
1196
1649
|
await this.cleanupExecution({ tags });
|
|
1197
1650
|
await this.initExecution({ tags });
|
|
1198
1651
|
}
|
|
1199
|
-
}
|
|
1200
1652
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1653
|
+
parseFeatureFile(featureFilePath) {
|
|
1654
|
+
try {
|
|
1655
|
+
let id = 0;
|
|
1656
|
+
const uuidFn = () => (++id).toString(16);
|
|
1657
|
+
const builder = new AstBuilder(uuidFn);
|
|
1658
|
+
const matcher = new GherkinClassicTokenMatcher();
|
|
1659
|
+
const parser = new Parser(builder, matcher);
|
|
1660
|
+
const source = readFileSync(featureFilePath, "utf8");
|
|
1661
|
+
const gherkinDocument = parser.parse(source);
|
|
1662
|
+
return gherkinDocument;
|
|
1663
|
+
} catch (e) {
|
|
1664
|
+
this.logger.error(`Error parsing feature file: ${featureFilePath}`);
|
|
1665
|
+
console.log(e);
|
|
1666
|
+
}
|
|
1667
|
+
return {};
|
|
1213
1668
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1669
|
+
|
|
1670
|
+
stopRecordingNetwork(input) {
|
|
1671
|
+
if (this.bvtContext) {
|
|
1672
|
+
this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
async fakeParams(params) {
|
|
1677
|
+
const newFakeParams = {};
|
|
1678
|
+
Object.keys(params).forEach((key) => {
|
|
1679
|
+
if (!params[key].startsWith("{{") || !params[key].endsWith("}}")) {
|
|
1680
|
+
newFakeParams[key] = params[key];
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
try {
|
|
1685
|
+
const value = params[key].substring(2, params[key].length - 2).trim();
|
|
1686
|
+
const faking = value.split("(")[0].split(".");
|
|
1687
|
+
let argument = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
|
|
1688
|
+
argument = isNaN(Number(argument)) || argument === "" ? argument : Number(argument);
|
|
1689
|
+
let fakeFunc = faker;
|
|
1690
|
+
faking.forEach((f) => {
|
|
1691
|
+
fakeFunc = fakeFunc[f];
|
|
1692
|
+
});
|
|
1693
|
+
const newValue = fakeFunc(argument);
|
|
1694
|
+
newFakeParams[key] = newValue;
|
|
1695
|
+
} catch (error) {
|
|
1696
|
+
newFakeParams[key] = params[key];
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
return newFakeParams;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
async getBrowserState() {
|
|
1704
|
+
try {
|
|
1705
|
+
const state = await this.browserEmitter?.getState();
|
|
1706
|
+
this.sendEvent(this.events.browserStateSync, state);
|
|
1707
|
+
} catch (error) {
|
|
1708
|
+
this.logger.error("Error getting browser state:", error);
|
|
1709
|
+
this.sendEvent(this.events.browserStateError, {
|
|
1710
|
+
message: "Error getting browser state",
|
|
1711
|
+
code: "GET_STATE_ERROR",
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
async applyClipboardPayload(message) {
|
|
1717
|
+
const payload = message?.data ?? message;
|
|
1718
|
+
|
|
1719
|
+
this.logger.info("[BVTRecorder] applyClipboardPayload called", {
|
|
1720
|
+
hasPayload: !!payload,
|
|
1721
|
+
hasText: !!payload?.text,
|
|
1722
|
+
hasHtml: !!payload?.html,
|
|
1723
|
+
trigger: message?.trigger,
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
if (!payload) {
|
|
1727
|
+
this.logger.warn("[BVTRecorder] No payload provided");
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
try {
|
|
1732
|
+
if (this.browserEmitter && typeof this.browserEmitter.applyClipboardPayload === "function") {
|
|
1733
|
+
this.logger.info("[BVTRecorder] Using RemoteBrowserService to apply clipboard");
|
|
1734
|
+
await this.browserEmitter.applyClipboardPayload(payload);
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
1739
|
+
if (!activePage) {
|
|
1740
|
+
this.logger.warn("[BVTRecorder] No active page available");
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
this.logger.info("[BVTRecorder] Applying clipboard to page", {
|
|
1745
|
+
url: activePage.url(),
|
|
1746
|
+
isClosed: activePage.isClosed(),
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
const result = await activePage.evaluate((clipboardData) => {
|
|
1750
|
+
console.log("[Page] Executing clipboard application", clipboardData);
|
|
1751
|
+
if (typeof window.__bvt_applyClipboardData === "function") {
|
|
1752
|
+
return window.__bvt_applyClipboardData(clipboardData);
|
|
1753
|
+
}
|
|
1754
|
+
console.error("[Page] __bvt_applyClipboardData function not found!");
|
|
1755
|
+
return false;
|
|
1756
|
+
}, payload);
|
|
1757
|
+
|
|
1758
|
+
this.logger.info("[BVTRecorder] Clipboard application result:", result);
|
|
1759
|
+
|
|
1760
|
+
if (!result) {
|
|
1761
|
+
this.logger.warn("[BVTRecorder] Clipboard data not applied successfully");
|
|
1762
|
+
} else {
|
|
1763
|
+
this.logger.info("[BVTRecorder] Clipboard data applied successfully");
|
|
1764
|
+
}
|
|
1765
|
+
} catch (error) {
|
|
1766
|
+
this.logger.error("[BVTRecorder] Error applying clipboard payload", error);
|
|
1767
|
+
this.sendEvent(this.events.clipboardError, {
|
|
1768
|
+
message: "Failed to apply clipboard contents to the remote session",
|
|
1769
|
+
trigger: message?.trigger ?? "paste",
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
hasClipboardPayload(payload) {
|
|
1775
|
+
return Boolean(
|
|
1776
|
+
payload && (payload.text || payload.html || (Array.isArray(payload.files) && payload.files.length > 0))
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
async collectClipboardFromPage(page) {
|
|
1781
|
+
if (!page) {
|
|
1782
|
+
this.logger.warn("[BVTRecorder] No page available to collect clipboard data");
|
|
1783
|
+
return null;
|
|
1784
|
+
}
|
|
1785
|
+
try {
|
|
1786
|
+
await page
|
|
1787
|
+
.context()
|
|
1788
|
+
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
1789
|
+
.catch((error) => {
|
|
1790
|
+
this.logger.warn("[BVTRecorder] Failed to grant clipboard permissions before read", error);
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1793
|
+
const payload = await page.evaluate(async () => {
|
|
1794
|
+
const result = {};
|
|
1795
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
|
1796
|
+
return result;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
const arrayBufferToBase64 = (buffer) => {
|
|
1800
|
+
let binary = "";
|
|
1801
|
+
const bytes = new Uint8Array(buffer);
|
|
1802
|
+
const chunkSize = 0x8000;
|
|
1803
|
+
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
1804
|
+
const chunk = bytes.subarray(index, index + chunkSize);
|
|
1805
|
+
binary += String.fromCharCode(...chunk);
|
|
1806
|
+
}
|
|
1807
|
+
return btoa(binary);
|
|
1808
|
+
};
|
|
1809
|
+
|
|
1810
|
+
const files = [];
|
|
1811
|
+
|
|
1812
|
+
if (typeof navigator.clipboard.read === "function") {
|
|
1813
|
+
try {
|
|
1814
|
+
const items = await navigator.clipboard.read();
|
|
1815
|
+
for (const item of items) {
|
|
1816
|
+
if (item.types.includes("text/html") && !result.html) {
|
|
1817
|
+
const blob = await item.getType("text/html");
|
|
1818
|
+
result.html = await blob.text();
|
|
1819
|
+
}
|
|
1820
|
+
if (item.types.includes("text/plain") && !result.text) {
|
|
1821
|
+
const blob = await item.getType("text/plain");
|
|
1822
|
+
result.text = await blob.text();
|
|
1823
|
+
}
|
|
1824
|
+
for (const type of item.types) {
|
|
1825
|
+
if (type.startsWith("text/")) {
|
|
1826
|
+
continue;
|
|
1827
|
+
}
|
|
1828
|
+
try {
|
|
1829
|
+
const blob = await item.getType(type);
|
|
1830
|
+
const buffer = await blob.arrayBuffer();
|
|
1831
|
+
files.push({
|
|
1832
|
+
name: `clipboard-file-${files.length + 1}`,
|
|
1833
|
+
type,
|
|
1834
|
+
lastModified: Date.now(),
|
|
1835
|
+
data: arrayBufferToBase64(buffer),
|
|
1836
|
+
});
|
|
1837
|
+
} catch (error) {
|
|
1838
|
+
console.warn("[BVTRecorder] Failed to serialize clipboard blob", { type, error });
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
} catch (error) {
|
|
1843
|
+
console.warn("[BVTRecorder] navigator.clipboard.read failed", error);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
if (!result.text && typeof navigator.clipboard.readText === "function") {
|
|
1848
|
+
try {
|
|
1849
|
+
const text = await navigator.clipboard.readText();
|
|
1850
|
+
if (text) {
|
|
1851
|
+
result.text = text;
|
|
1852
|
+
}
|
|
1853
|
+
} catch (error) {
|
|
1854
|
+
console.warn("[BVTRecorder] navigator.clipboard.readText failed", error);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
if (!result.text) {
|
|
1859
|
+
const selection = window.getSelection?.()?.toString?.();
|
|
1860
|
+
if (selection) {
|
|
1861
|
+
result.text = selection;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
if (files.length > 0) {
|
|
1866
|
+
result.files = files;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
return result;
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
return payload;
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
this.logger.error("[BVTRecorder] Error collecting clipboard payload", error);
|
|
1875
|
+
return null;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
async readClipboardPayload(message) {
|
|
1880
|
+
try {
|
|
1881
|
+
let payload = null;
|
|
1882
|
+
if (this.browserEmitter && typeof this.browserEmitter.readClipboardPayload === "function") {
|
|
1883
|
+
payload = await this.browserEmitter.readClipboardPayload();
|
|
1884
|
+
} else {
|
|
1885
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
1886
|
+
payload = await this.collectClipboardFromPage(activePage);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
if (this.hasClipboardPayload(payload)) {
|
|
1890
|
+
this.logger.info("[BVTRecorder] Remote clipboard payload ready", {
|
|
1891
|
+
hasText: !!payload.text,
|
|
1892
|
+
hasHtml: !!payload.html,
|
|
1893
|
+
files: payload.files?.length ?? 0,
|
|
1894
|
+
});
|
|
1895
|
+
this.sendEvent(this.events.clipboardPush, {
|
|
1896
|
+
data: payload,
|
|
1897
|
+
trigger: message?.trigger ?? "copy",
|
|
1898
|
+
origin: message?.source ?? "browserUI",
|
|
1899
|
+
});
|
|
1900
|
+
return payload;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
this.logger.warn("[BVTRecorder] Remote clipboard payload empty or unavailable");
|
|
1904
|
+
this.sendEvent(this.events.clipboardError, {
|
|
1905
|
+
message: "Remote clipboard is empty",
|
|
1906
|
+
trigger: message?.trigger ?? "copy",
|
|
1907
|
+
});
|
|
1908
|
+
return null;
|
|
1909
|
+
} catch (error) {
|
|
1910
|
+
this.logger.error("[BVTRecorder] Error reading clipboard payload", error);
|
|
1911
|
+
this.sendEvent(this.events.clipboardError, {
|
|
1912
|
+
message: "Failed to read clipboard contents from the remote session",
|
|
1913
|
+
trigger: message?.trigger ?? "copy",
|
|
1914
|
+
details: error instanceof Error ? error.message : String(error),
|
|
1915
|
+
});
|
|
1916
|
+
throw error;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
async injectClipboardIntoPage(page, payload) {
|
|
1921
|
+
if (!page) {
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
try {
|
|
1926
|
+
await page
|
|
1927
|
+
.context()
|
|
1928
|
+
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
1929
|
+
.catch(() => {});
|
|
1930
|
+
await page.evaluate(async (clipboardPayload) => {
|
|
1931
|
+
const toArrayBuffer = (base64) => {
|
|
1932
|
+
if (!base64) {
|
|
1933
|
+
return null;
|
|
1934
|
+
}
|
|
1935
|
+
const binaryString = atob(base64);
|
|
1936
|
+
const len = binaryString.length;
|
|
1937
|
+
const bytes = new Uint8Array(len);
|
|
1938
|
+
for (let i = 0; i < len; i += 1) {
|
|
1939
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
1940
|
+
}
|
|
1941
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
1942
|
+
};
|
|
1943
|
+
|
|
1944
|
+
const createFileFromPayload = (filePayload) => {
|
|
1945
|
+
const buffer = toArrayBuffer(filePayload?.data);
|
|
1946
|
+
if (!buffer) {
|
|
1947
|
+
return null;
|
|
1948
|
+
}
|
|
1949
|
+
const name = filePayload?.name || "clipboard-file";
|
|
1950
|
+
const type = filePayload?.type || "application/octet-stream";
|
|
1951
|
+
const lastModified = filePayload?.lastModified ?? Date.now();
|
|
1952
|
+
try {
|
|
1953
|
+
return new File([buffer], name, { type, lastModified });
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
console.warn("Clipboard bridge could not recreate File object", error);
|
|
1956
|
+
return null;
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
|
|
1960
|
+
let dataTransfer = null;
|
|
1961
|
+
try {
|
|
1962
|
+
dataTransfer = new DataTransfer();
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
console.warn("Clipboard bridge could not create DataTransfer", error);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
if (dataTransfer) {
|
|
1968
|
+
if (clipboardPayload?.text) {
|
|
1969
|
+
try {
|
|
1970
|
+
dataTransfer.setData("text/plain", clipboardPayload.text);
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
console.warn("Clipboard bridge failed to set text/plain", error);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
if (clipboardPayload?.html) {
|
|
1976
|
+
try {
|
|
1977
|
+
dataTransfer.setData("text/html", clipboardPayload.html);
|
|
1978
|
+
} catch (error) {
|
|
1979
|
+
console.warn("Clipboard bridge failed to set text/html", error);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
if (Array.isArray(clipboardPayload?.files)) {
|
|
1983
|
+
for (const filePayload of clipboardPayload.files) {
|
|
1984
|
+
const file = createFileFromPayload(filePayload);
|
|
1985
|
+
if (file) {
|
|
1986
|
+
try {
|
|
1987
|
+
dataTransfer.items.add(file);
|
|
1988
|
+
} catch (error) {
|
|
1989
|
+
console.warn("Clipboard bridge failed to append file", error);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
let target = document.activeElement || document.body;
|
|
1997
|
+
if (!target) {
|
|
1998
|
+
target = document.body || null;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
let pasteHandled = false;
|
|
2002
|
+
if (dataTransfer && target && typeof target.dispatchEvent === "function") {
|
|
2003
|
+
try {
|
|
2004
|
+
const clipboardEvent = new ClipboardEvent("paste", {
|
|
2005
|
+
clipboardData: dataTransfer,
|
|
2006
|
+
bubbles: true,
|
|
2007
|
+
cancelable: true,
|
|
2008
|
+
});
|
|
2009
|
+
pasteHandled = target.dispatchEvent(clipboardEvent);
|
|
2010
|
+
} catch (error) {
|
|
2011
|
+
console.warn("Clipboard bridge failed to dispatch synthetic paste event", error);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
if (pasteHandled) {
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
const callLegacyExecCommand = (command, value) => {
|
|
2020
|
+
const execCommand = document && document["execCommand"];
|
|
2021
|
+
if (typeof execCommand === "function") {
|
|
2022
|
+
try {
|
|
2023
|
+
return execCommand.call(document, command, false, value);
|
|
2024
|
+
} catch (error) {
|
|
2025
|
+
console.warn("Clipboard bridge failed to execute legacy command", error);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
return false;
|
|
2029
|
+
};
|
|
2030
|
+
|
|
2031
|
+
if (clipboardPayload?.html) {
|
|
2032
|
+
const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
|
|
2033
|
+
if (inserted) {
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
try {
|
|
2037
|
+
const selection = window.getSelection?.();
|
|
2038
|
+
if (selection && selection.rangeCount > 0) {
|
|
2039
|
+
const range = selection.getRangeAt(0);
|
|
2040
|
+
range.deleteContents();
|
|
2041
|
+
const fragment = range.createContextualFragment(clipboardPayload.html);
|
|
2042
|
+
range.insertNode(fragment);
|
|
2043
|
+
range.collapse(false);
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
} catch (error) {
|
|
2047
|
+
console.warn("Clipboard bridge could not insert HTML via Range APIs", error);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
if (clipboardPayload?.text) {
|
|
2052
|
+
const inserted = callLegacyExecCommand("insertText", clipboardPayload.text);
|
|
2053
|
+
if (inserted) {
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
try {
|
|
2057
|
+
const selection = window.getSelection?.();
|
|
2058
|
+
if (selection && selection.rangeCount > 0) {
|
|
2059
|
+
const range = selection.getRangeAt(0);
|
|
2060
|
+
range.deleteContents();
|
|
2061
|
+
range.insertNode(document.createTextNode(clipboardPayload.text));
|
|
2062
|
+
range.collapse(false);
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
} catch (error) {
|
|
2066
|
+
console.warn("Clipboard bridge could not insert text via Range APIs", error);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
if (clipboardPayload?.text && target && "value" in target) {
|
|
2071
|
+
try {
|
|
2072
|
+
const input = target;
|
|
2073
|
+
const start = input.selectionStart ?? input.value.length ?? 0;
|
|
2074
|
+
const end = input.selectionEnd ?? input.value.length ?? 0;
|
|
2075
|
+
const value = input.value ?? "";
|
|
2076
|
+
const text = clipboardPayload.text;
|
|
2077
|
+
input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
|
|
2078
|
+
const caret = start + text.length;
|
|
2079
|
+
if (typeof input.setSelectionRange === "function") {
|
|
2080
|
+
input.setSelectionRange(caret, caret);
|
|
2081
|
+
}
|
|
2082
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2083
|
+
} catch (error) {
|
|
2084
|
+
console.warn("Clipboard bridge failed to mutate input element", error);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}, payload);
|
|
2088
|
+
} catch (error) {
|
|
2089
|
+
throw error;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
async createTab(url) {
|
|
2094
|
+
try {
|
|
2095
|
+
await this.browserEmitter?.createTab(url);
|
|
2096
|
+
} catch (error) {
|
|
2097
|
+
this.logger.error("Error creating tab:", error);
|
|
2098
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2099
|
+
message: "Error creating tab",
|
|
2100
|
+
code: "CREATE_TAB_ERROR",
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
async closeTab(pageId) {
|
|
2106
|
+
try {
|
|
2107
|
+
await this.browserEmitter?.closeTab(pageId);
|
|
2108
|
+
} catch (error) {
|
|
2109
|
+
this.logger.error("Error closing tab:", error);
|
|
2110
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2111
|
+
message: "Error closing tab",
|
|
2112
|
+
code: "CLOSE_TAB_ERROR",
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
async selectTab(pageId) {
|
|
2118
|
+
try {
|
|
2119
|
+
await this.browserEmitter?.selectTab(pageId);
|
|
2120
|
+
} catch (error) {
|
|
2121
|
+
this.logger.error("Error selecting tab:", error);
|
|
2122
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2123
|
+
message: "Error selecting tab",
|
|
2124
|
+
code: "SELECT_TAB_ERROR",
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
async navigateTab({ pageId, url }) {
|
|
2130
|
+
try {
|
|
2131
|
+
if (!pageId || !url) {
|
|
2132
|
+
this.logger.error("navigateTab called without pageId or url", { pageId, url });
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
await this.browserEmitter?.navigateTab(pageId, url);
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
this.logger.error("Error navigating tab:", error);
|
|
2138
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2139
|
+
message: "Error navigating tab",
|
|
2140
|
+
code: "NAVIGATE_TAB_ERROR",
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
async reloadTab(pageId) {
|
|
2146
|
+
try {
|
|
2147
|
+
await this.browserEmitter?.reloadTab(pageId);
|
|
2148
|
+
} catch (error) {
|
|
2149
|
+
this.logger.error("Error reloading tab:", error);
|
|
2150
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2151
|
+
message: "Error reloading tab",
|
|
2152
|
+
code: "RELOAD_TAB_ERROR",
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
async goBack(pageId) {
|
|
2158
|
+
try {
|
|
2159
|
+
await this.browserEmitter?.goBack(pageId);
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
this.logger.error("Error navigating back:", error);
|
|
2162
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2163
|
+
message: "Error navigating back",
|
|
2164
|
+
code: "GO_BACK_ERROR",
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
async goForward(pageId) {
|
|
2170
|
+
try {
|
|
2171
|
+
await this.browserEmitter?.goForward(pageId);
|
|
2172
|
+
} catch (error) {
|
|
2173
|
+
this.logger.error("Error navigating forward:", error);
|
|
2174
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2175
|
+
message: "Error navigating forward",
|
|
2176
|
+
code: "GO_FORWARD_ERROR",
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
}
|