@dev-blinq/cucumber_client 1.0.1406-dev → 1.0.1406-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 +1038 -76
- package/bin/client/recorderv3/implemented_steps.js +2 -0
- package/bin/client/recorderv3/index.js +4 -311
- package/bin/client/recorderv3/scriptTest.js +1 -1
- package/bin/client/recorderv3/services.js +814 -154
- package/bin/client/recorderv3/step_runner.js +36 -10
- 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,12 +504,19 @@ export class BVTRecorder {
|
|
|
184
504
|
projectDir: this.projectDir,
|
|
185
505
|
logger: this.logger,
|
|
186
506
|
});
|
|
507
|
+
this.workspaceService = new PublishService(this.TOKEN);
|
|
187
508
|
this.pageSet = new Set();
|
|
188
509
|
this.lastKnownUrlPath = "";
|
|
189
|
-
// TODO: what is world?
|
|
190
510
|
this.world = { attach: () => {} };
|
|
191
511
|
this.shouldTakeScreenshot = true;
|
|
192
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
|
+
}
|
|
193
520
|
}
|
|
194
521
|
events = {
|
|
195
522
|
onFrameNavigate: "BVTRecorder.onFrameNavigate",
|
|
@@ -204,12 +531,18 @@ export class BVTRecorder {
|
|
|
204
531
|
cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
|
|
205
532
|
cmdExecutionError: "BVTRecorder.cmdExecutionError",
|
|
206
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",
|
|
207
540
|
};
|
|
208
541
|
bindings = {
|
|
209
542
|
__bvt_recordCommand: async ({ frame, page, context }, event) => {
|
|
210
543
|
this.#activeFrame = frame;
|
|
211
544
|
const nestFrmLoc = await findNestedFrameSelector(frame);
|
|
212
|
-
|
|
545
|
+
this.logger.info(`Time taken for action: ${event.statistics.time}`);
|
|
213
546
|
await this.onAction({ ...event, nestFrmLoc });
|
|
214
547
|
},
|
|
215
548
|
__bvt_getMode: async () => {
|
|
@@ -228,12 +561,30 @@ export class BVTRecorder {
|
|
|
228
561
|
await this.onClosePopup();
|
|
229
562
|
},
|
|
230
563
|
__bvt_log: async (src, message) => {
|
|
231
|
-
|
|
232
|
-
console.log(`Inside Browser: ${message}`);
|
|
564
|
+
this.logger.info(`Inside Browser: ${message}`);
|
|
233
565
|
},
|
|
234
566
|
__bvt_getObject: (_src, obj) => {
|
|
235
567
|
this.processObject(obj);
|
|
236
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
|
+
},
|
|
237
588
|
};
|
|
238
589
|
|
|
239
590
|
getSnapshot = async (attr) => {
|
|
@@ -291,8 +642,12 @@ export class BVTRecorder {
|
|
|
291
642
|
}
|
|
292
643
|
|
|
293
644
|
async _initBrowser({ url }) {
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
}
|
|
296
651
|
|
|
297
652
|
// this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
|
|
298
653
|
this.world = { attach: () => {} };
|
|
@@ -303,7 +658,7 @@ export class BVTRecorder {
|
|
|
303
658
|
try {
|
|
304
659
|
ai_config = JSON.parse(readFileSync(ai_config_file, "utf8"));
|
|
305
660
|
} catch (error) {
|
|
306
|
-
|
|
661
|
+
this.logger.error("Error reading ai_config.json", error);
|
|
307
662
|
}
|
|
308
663
|
}
|
|
309
664
|
this.config = ai_config;
|
|
@@ -315,10 +670,8 @@ export class BVTRecorder {
|
|
|
315
670
|
],
|
|
316
671
|
};
|
|
317
672
|
|
|
318
|
-
|
|
319
|
-
const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName);
|
|
320
|
-
let stopTime = Date.now();
|
|
321
|
-
this.logger.info(`Browser launched in ${(stopTime - startTime) / 1000} s`);
|
|
673
|
+
const scenario = { pickle: this.scenarioDoc };
|
|
674
|
+
const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName, scenario);
|
|
322
675
|
this.bvtContext = bvtContext;
|
|
323
676
|
this.stepRunner = new BVTStepRunner({
|
|
324
677
|
projectDir: this.projectDir,
|
|
@@ -326,35 +679,40 @@ export class BVTRecorder {
|
|
|
326
679
|
if (data && data.type) {
|
|
327
680
|
switch (data.type) {
|
|
328
681
|
case "cmdExecutionStart":
|
|
329
|
-
console.log("Sending cmdExecutionStart event for cmdId:", data);
|
|
330
682
|
this.sendEvent(this.events.cmdExecutionStart, data);
|
|
331
683
|
break;
|
|
332
684
|
case "cmdExecutionSuccess":
|
|
333
|
-
console.log("Sending cmdExecutionSuccess event for cmdId:", data);
|
|
334
685
|
this.sendEvent(this.events.cmdExecutionSuccess, data);
|
|
335
686
|
break;
|
|
336
687
|
case "cmdExecutionError":
|
|
337
|
-
console.log("Sending cmdExecutionError event for cmdId:", data);
|
|
338
688
|
this.sendEvent(this.events.cmdExecutionError, data);
|
|
339
689
|
break;
|
|
340
690
|
case "interceptResults":
|
|
341
|
-
console.log("Sending interceptResults event");
|
|
342
691
|
this.sendEvent(this.events.interceptResults, data);
|
|
343
692
|
break;
|
|
344
693
|
default:
|
|
345
|
-
console.warn("Unknown command execution status type:", data.type);
|
|
346
694
|
break;
|
|
347
695
|
}
|
|
348
696
|
}
|
|
349
697
|
},
|
|
350
698
|
bvtContext: this.bvtContext,
|
|
351
699
|
});
|
|
352
|
-
|
|
353
|
-
this.context = context;
|
|
700
|
+
this.context = bvtContext.playContext;
|
|
354
701
|
this.web = bvtContext.stable || bvtContext.web;
|
|
355
702
|
this.web.tryAllStrategies = true;
|
|
356
703
|
this.page = bvtContext.page;
|
|
357
704
|
this.pageSet.add(this.page);
|
|
705
|
+
if (process.env.REMOTE_RECORDER === "true") {
|
|
706
|
+
this.browserEmitter = new RemoteBrowserService({
|
|
707
|
+
CDP_CONNECT_URL: `http://localhost:${this.#remoteDebuggerPort}`,
|
|
708
|
+
context: this.context,
|
|
709
|
+
});
|
|
710
|
+
this.browserEmitter.on(this.events.browserStateSync, (state) => {
|
|
711
|
+
this.page = this.browserEmitter.getSelectedPage();
|
|
712
|
+
this.sendEvent(this.events.browserStateSync, state);
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
358
716
|
this.lastKnownUrlPath = this._updateUrlPath();
|
|
359
717
|
const browser = await this.context.browser();
|
|
360
718
|
this.browser = browser;
|
|
@@ -367,6 +725,14 @@ export class BVTRecorder {
|
|
|
367
725
|
this.web.onRestoreSaveState = (url) => {
|
|
368
726
|
this._initBrowser({ url });
|
|
369
727
|
};
|
|
728
|
+
|
|
729
|
+
// create a second browser for locator generation
|
|
730
|
+
this.backgroundBrowser = await chromium.launch({
|
|
731
|
+
headless: true,
|
|
732
|
+
});
|
|
733
|
+
this.backgroundContext = await this.backgroundBrowser.newContext({});
|
|
734
|
+
await this.backgroundContext.addInitScript({ content: this.getInitScripts(this.config) });
|
|
735
|
+
await this.backgroundContext.newPage();
|
|
370
736
|
}
|
|
371
737
|
async onClosePopup() {
|
|
372
738
|
// console.log("close popups");
|
|
@@ -381,13 +747,15 @@ export class BVTRecorder {
|
|
|
381
747
|
}
|
|
382
748
|
return;
|
|
383
749
|
} catch (error) {
|
|
384
|
-
console.error("Error evaluting in context:", error);
|
|
750
|
+
// console.error("Error evaluting in context:", error);
|
|
751
|
+
this.logger.error("Error evaluating in context:", error);
|
|
385
752
|
}
|
|
386
753
|
}
|
|
387
754
|
}
|
|
388
755
|
|
|
389
756
|
getMode() {
|
|
390
|
-
console.log("getMode", this.#mode);
|
|
757
|
+
// console.log("getMode", this.#mode);
|
|
758
|
+
this.logger.info("Current mode:", this.#mode);
|
|
391
759
|
return this.#mode;
|
|
392
760
|
}
|
|
393
761
|
|
|
@@ -406,13 +774,14 @@ export class BVTRecorder {
|
|
|
406
774
|
|
|
407
775
|
await this.page.goto(url, {
|
|
408
776
|
waitUntil: "domcontentloaded",
|
|
777
|
+
timeout: this.config.page_timeout ?? 60_000,
|
|
409
778
|
});
|
|
410
779
|
// add listener for frame navigation on current tab
|
|
411
780
|
this._addFrameNavigateListener(this.page);
|
|
412
781
|
|
|
413
782
|
// eval init script on current tab
|
|
414
783
|
// await this._initPage(this.page);
|
|
415
|
-
this.#currentURL =
|
|
784
|
+
this.#currentURL = url;
|
|
416
785
|
|
|
417
786
|
await this.page.dispatchEvent("html", "scroll");
|
|
418
787
|
await delay(1000);
|
|
@@ -429,6 +798,8 @@ export class BVTRecorder {
|
|
|
429
798
|
this.sendEvent(this.events.onBrowserClose);
|
|
430
799
|
}
|
|
431
800
|
} catch (error) {
|
|
801
|
+
this.logger.error("Error in page close event");
|
|
802
|
+
this.logger.error(error);
|
|
432
803
|
console.error("Error in page close event");
|
|
433
804
|
console.error(error);
|
|
434
805
|
}
|
|
@@ -439,8 +810,10 @@ export class BVTRecorder {
|
|
|
439
810
|
if (frame !== page.mainFrame()) return;
|
|
440
811
|
this.handlePageTransition();
|
|
441
812
|
} catch (error) {
|
|
813
|
+
this.logger.error("Error in handlePageTransition event");
|
|
814
|
+
this.logger.error(error);
|
|
442
815
|
console.error("Error in handlePageTransition event");
|
|
443
|
-
|
|
816
|
+
console.error(error);
|
|
444
817
|
}
|
|
445
818
|
try {
|
|
446
819
|
if (frame !== this.#activeFrame) return;
|
|
@@ -450,15 +823,18 @@ export class BVTRecorder {
|
|
|
450
823
|
element: { inputID: "frame" },
|
|
451
824
|
});
|
|
452
825
|
|
|
453
|
-
const
|
|
826
|
+
const newUrl = frame.url();
|
|
827
|
+
const newPath = new URL(newUrl).pathname;
|
|
454
828
|
const newTitle = await frame.title();
|
|
455
|
-
|
|
829
|
+
const changed = diffPaths(this.#currentURL, newUrl);
|
|
830
|
+
|
|
831
|
+
if (changed) {
|
|
456
832
|
this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
|
|
457
|
-
this.#currentURL =
|
|
833
|
+
this.#currentURL = newUrl;
|
|
458
834
|
}
|
|
459
|
-
// await this._setRecordingMode(frame);
|
|
460
|
-
// await this._initPage(page);
|
|
461
835
|
} catch (error) {
|
|
836
|
+
this.logger.error("Error in frame navigate event");
|
|
837
|
+
this.logger.error(error);
|
|
462
838
|
console.error("Error in frame navigate event");
|
|
463
839
|
// console.error(error);
|
|
464
840
|
}
|
|
@@ -541,13 +917,9 @@ export class BVTRecorder {
|
|
|
541
917
|
|
|
542
918
|
try {
|
|
543
919
|
const result = await client.send("Page.getNavigationHistory");
|
|
544
|
-
// console.log("Navigation History:", result);
|
|
545
920
|
const entries = result.entries;
|
|
546
921
|
const currentIndex = result.currentIndex;
|
|
547
922
|
|
|
548
|
-
// ignore if currentIndex is not the last entry
|
|
549
|
-
// if (currentIndex !== entries.length - 1) return;
|
|
550
|
-
|
|
551
923
|
const currentEntry = entries[currentIndex];
|
|
552
924
|
const transitionInfo = this.analyzeTransitionType(entries, currentIndex, currentEntry);
|
|
553
925
|
this.previousIndex = currentIndex;
|
|
@@ -560,6 +932,8 @@ export class BVTRecorder {
|
|
|
560
932
|
navigationAction: transitionInfo.action,
|
|
561
933
|
};
|
|
562
934
|
} catch (error) {
|
|
935
|
+
this.logger.error("Error in getCurrentTransition event");
|
|
936
|
+
this.logger.error(error);
|
|
563
937
|
console.error("Error in getTransistionType event", error);
|
|
564
938
|
} finally {
|
|
565
939
|
await client.detach();
|
|
@@ -622,12 +996,13 @@ export class BVTRecorder {
|
|
|
622
996
|
try {
|
|
623
997
|
if (page.isClosed()) return;
|
|
624
998
|
this.pageSet.add(page);
|
|
625
|
-
|
|
626
999
|
await page.waitForLoadState("domcontentloaded");
|
|
627
1000
|
|
|
628
1001
|
// add listener for frame navigation on new tab
|
|
629
1002
|
this._addFrameNavigateListener(page);
|
|
630
1003
|
} catch (error) {
|
|
1004
|
+
this.logger.error("Error in page event");
|
|
1005
|
+
this.logger.error(error);
|
|
631
1006
|
console.error("Error in page event");
|
|
632
1007
|
console.error(error);
|
|
633
1008
|
}
|
|
@@ -669,6 +1044,7 @@ export class BVTRecorder {
|
|
|
669
1044
|
const { data } = await client.send("Page.captureScreenshot", { format: "png" });
|
|
670
1045
|
return data;
|
|
671
1046
|
} catch (error) {
|
|
1047
|
+
this.logger.error("Error in taking browser screenshot");
|
|
672
1048
|
console.error("Error in taking browser screenshot", error);
|
|
673
1049
|
} finally {
|
|
674
1050
|
await client.detach();
|
|
@@ -684,6 +1060,52 @@ export class BVTRecorder {
|
|
|
684
1060
|
console.error("Error in saving screenshot: ", error);
|
|
685
1061
|
}
|
|
686
1062
|
}
|
|
1063
|
+
async generateLocators(event) {
|
|
1064
|
+
const snapshotDetails = event.snapshotDetails;
|
|
1065
|
+
if (!snapshotDetails) {
|
|
1066
|
+
throw new Error("No snapshot details found");
|
|
1067
|
+
}
|
|
1068
|
+
const mode = event.mode;
|
|
1069
|
+
const inputID = event.element.inputID;
|
|
1070
|
+
|
|
1071
|
+
const { id, contextId, doc } = snapshotDetails;
|
|
1072
|
+
// const selector = `[data-blinq-id="${id}"]`;
|
|
1073
|
+
const newPage = await this.backgroundContext.newPage();
|
|
1074
|
+
await newPage.setContent(doc, { waitUntil: "domcontentloaded" });
|
|
1075
|
+
const locatorsObj = await newPage.evaluate(
|
|
1076
|
+
([id, contextId, mode]) => {
|
|
1077
|
+
const recorder = window.__bvt_Recorder;
|
|
1078
|
+
const contextElement = document.querySelector(`[data-blinq-context-id="${contextId}"]`);
|
|
1079
|
+
const el = document.querySelector(`[data-blinq-id="${id}"]`);
|
|
1080
|
+
if (contextElement) {
|
|
1081
|
+
const result = recorder.locatorGenerator.toContextLocators(el, contextElement);
|
|
1082
|
+
return result;
|
|
1083
|
+
}
|
|
1084
|
+
const isRecordingText = mode === "recordingText";
|
|
1085
|
+
return recorder.locatorGenerator.getElementLocators(el, {
|
|
1086
|
+
excludeText: isRecordingText,
|
|
1087
|
+
});
|
|
1088
|
+
},
|
|
1089
|
+
[id, contextId, mode]
|
|
1090
|
+
);
|
|
1091
|
+
|
|
1092
|
+
// console.log(`Generated locators: for ${inputID}: `, JSON.stringify(locatorsObj));
|
|
1093
|
+
await newPage.close();
|
|
1094
|
+
if (event.nestFrmLoc?.children) {
|
|
1095
|
+
locatorsObj.nestFrmLoc = event.nestFrmLoc.children;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
this.sendEvent(this.events.updateCommand, {
|
|
1099
|
+
locators: {
|
|
1100
|
+
locators: locatorsObj.locators,
|
|
1101
|
+
nestFrmLoc: locatorsObj.nestFrmLoc,
|
|
1102
|
+
iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
1103
|
+
},
|
|
1104
|
+
allStrategyLocators: locatorsObj.allStrategyLocators,
|
|
1105
|
+
inputID,
|
|
1106
|
+
});
|
|
1107
|
+
// const
|
|
1108
|
+
}
|
|
687
1109
|
async onAction(event) {
|
|
688
1110
|
this._updateUrlPath();
|
|
689
1111
|
// const locators = this.overlayLocators(event);
|
|
@@ -697,25 +1119,26 @@ export class BVTRecorder {
|
|
|
697
1119
|
event.mode === "recordingHover",
|
|
698
1120
|
event.mode === "multiInspecting"
|
|
699
1121
|
),
|
|
700
|
-
locators: {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
},
|
|
704
|
-
allStrategyLocators: event.allStrategyLocators,
|
|
1122
|
+
// locators: {
|
|
1123
|
+
// locators: event.locators,
|
|
1124
|
+
// iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
1125
|
+
// },
|
|
1126
|
+
// allStrategyLocators: event.allStrategyLocators,
|
|
705
1127
|
url: event.frame.url,
|
|
706
1128
|
title: event.frame.title,
|
|
707
1129
|
extract: {},
|
|
708
1130
|
lastKnownUrlPath: this.lastKnownUrlPath,
|
|
709
1131
|
};
|
|
710
|
-
if (event.nestFrmLoc?.children) {
|
|
711
|
-
|
|
712
|
-
}
|
|
1132
|
+
// if (event.nestFrmLoc?.children) {
|
|
1133
|
+
// cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
|
|
1134
|
+
// }
|
|
713
1135
|
// this.logger.info({ event });
|
|
714
1136
|
if (this.shouldTakeScreenshot) {
|
|
715
1137
|
await this.storeScreenshot(event);
|
|
716
1138
|
}
|
|
717
1139
|
this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
718
1140
|
this._updateUrlPath();
|
|
1141
|
+
await this.generateLocators(event);
|
|
719
1142
|
}
|
|
720
1143
|
_updateUrlPath() {
|
|
721
1144
|
try {
|
|
@@ -737,7 +1160,6 @@ export class BVTRecorder {
|
|
|
737
1160
|
this.previousHistoryLength = null;
|
|
738
1161
|
this.previousUrl = null;
|
|
739
1162
|
this.previousEntries = null;
|
|
740
|
-
|
|
741
1163
|
await closeContext();
|
|
742
1164
|
this.pageSet.clear();
|
|
743
1165
|
}
|
|
@@ -831,17 +1253,27 @@ export class BVTRecorder {
|
|
|
831
1253
|
}
|
|
832
1254
|
async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
|
|
833
1255
|
const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
|
|
1256
|
+
|
|
1257
|
+
const env = path.basename(this.envName, ".json");
|
|
834
1258
|
const _env = {
|
|
835
1259
|
TOKEN: this.TOKEN,
|
|
836
1260
|
TEMP_RUN: true,
|
|
837
1261
|
REPORT_FOLDER: this.bvtContext.reportFolder,
|
|
838
1262
|
BLINQ_ENV: this.envName,
|
|
839
|
-
|
|
840
|
-
|
|
1263
|
+
DEBUG: "blinq:route",
|
|
1264
|
+
// BVT_TEMP_SNAPSHOTS_FOLDER: step.isImplemented ? path.join(this.tempSnapshotsFolder, env) : undefined,
|
|
841
1265
|
};
|
|
1266
|
+
if (!step.isImplemented) {
|
|
1267
|
+
_env.BVT_TEMP_SNAPSHOTS_FOLDER = path.join(this.tempSnapshotsFolder, env);
|
|
1268
|
+
}
|
|
842
1269
|
|
|
843
1270
|
this.bvtContext.navigate = true;
|
|
844
1271
|
this.bvtContext.loadedRoutes = null;
|
|
1272
|
+
if (listenNetwork) {
|
|
1273
|
+
this.bvtContext.STORE_DETAILED_NETWORK_DATA = true;
|
|
1274
|
+
} else {
|
|
1275
|
+
this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
|
|
1276
|
+
}
|
|
845
1277
|
for (const [key, value] of Object.entries(_env)) {
|
|
846
1278
|
process.env[key] = value;
|
|
847
1279
|
}
|
|
@@ -879,10 +1311,24 @@ export class BVTRecorder {
|
|
|
879
1311
|
this.bvtContext.navigate = false;
|
|
880
1312
|
}
|
|
881
1313
|
}
|
|
882
|
-
async saveScenario({ scenario, featureName, override, isSingleStep }) {
|
|
883
|
-
await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
|
|
884
|
-
if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
|
|
885
|
-
await this.
|
|
1314
|
+
async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env }) {
|
|
1315
|
+
// await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
|
|
1316
|
+
// if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
|
|
1317
|
+
const res = await this.workspaceService.saveScenario({
|
|
1318
|
+
scenario,
|
|
1319
|
+
featureName,
|
|
1320
|
+
override,
|
|
1321
|
+
isSingleStep,
|
|
1322
|
+
branch,
|
|
1323
|
+
isEditing,
|
|
1324
|
+
projectId: path.basename(this.projectDir),
|
|
1325
|
+
env: env ?? this.envName,
|
|
1326
|
+
});
|
|
1327
|
+
if (res.success) {
|
|
1328
|
+
await this.cleanup({ tags: scenario.tags });
|
|
1329
|
+
} else {
|
|
1330
|
+
throw new Error(res.message || "Error saving scenario");
|
|
1331
|
+
}
|
|
886
1332
|
}
|
|
887
1333
|
async getImplementedSteps() {
|
|
888
1334
|
const stepsAndScenarios = await getImplementedSteps(this.projectDir);
|
|
@@ -966,10 +1412,11 @@ export class BVTRecorder {
|
|
|
966
1412
|
if (existsSync(_getDataFile(this.world, this.bvtContext, this.web))) {
|
|
967
1413
|
try {
|
|
968
1414
|
const testData = JSON.parse(readFileSync(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
|
|
969
|
-
this.logger.info("Test data", testData);
|
|
1415
|
+
// this.logger.info("Test data", testData);
|
|
970
1416
|
this.sendEvent(this.events.getTestData, testData);
|
|
971
1417
|
} catch (e) {
|
|
972
|
-
this.logger.error("Error reading test data file", e);
|
|
1418
|
+
// this.logger.error("Error reading test data file", e);
|
|
1419
|
+
console.log("Error reading test data file", e);
|
|
973
1420
|
}
|
|
974
1421
|
}
|
|
975
1422
|
|
|
@@ -978,10 +1425,12 @@ export class BVTRecorder {
|
|
|
978
1425
|
this.watcher.on("all", async (event, path) => {
|
|
979
1426
|
try {
|
|
980
1427
|
const testData = JSON.parse(await readFile(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
|
|
981
|
-
this.logger.info("Test data", testData);
|
|
1428
|
+
// this.logger.info("Test data", testData);
|
|
1429
|
+
console.log("Test data changed", testData);
|
|
982
1430
|
this.sendEvent(this.events.getTestData, testData);
|
|
983
1431
|
} catch (e) {
|
|
984
|
-
this.logger.error("Error reading test data file", e);
|
|
1432
|
+
// this.logger.error("Error reading test data file", e);
|
|
1433
|
+
console.log("Error reading test data file", e);
|
|
985
1434
|
}
|
|
986
1435
|
});
|
|
987
1436
|
}
|
|
@@ -1014,7 +1463,7 @@ export class BVTRecorder {
|
|
|
1014
1463
|
.filter((file) => file.endsWith(".feature"))
|
|
1015
1464
|
.map((file) => path.join(this.projectDir, "features", file));
|
|
1016
1465
|
try {
|
|
1017
|
-
const parsedFiles = featureFiles.map((file) => parseFeatureFile(file));
|
|
1466
|
+
const parsedFiles = featureFiles.map((file) => this.parseFeatureFile(file));
|
|
1018
1467
|
const output = {};
|
|
1019
1468
|
parsedFiles.forEach((file) => {
|
|
1020
1469
|
if (!file.feature) return;
|
|
@@ -1042,8 +1491,9 @@ export class BVTRecorder {
|
|
|
1042
1491
|
loadExistingScenario({ featureName, scenarioName }) {
|
|
1043
1492
|
const step_definitions = loadStepDefinitions(this.projectDir);
|
|
1044
1493
|
const featureFilePath = path.join(this.projectDir, "features", featureName);
|
|
1045
|
-
const gherkinDoc = parseFeatureFile(featureFilePath);
|
|
1494
|
+
const gherkinDoc = this.parseFeatureFile(featureFilePath);
|
|
1046
1495
|
const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
|
|
1496
|
+
this.scenarioDoc = scenario;
|
|
1047
1497
|
|
|
1048
1498
|
const steps = [];
|
|
1049
1499
|
const parameters = [];
|
|
@@ -1201,20 +1651,532 @@ export class BVTRecorder {
|
|
|
1201
1651
|
await this.cleanupExecution({ tags });
|
|
1202
1652
|
await this.initExecution({ tags });
|
|
1203
1653
|
}
|
|
1204
|
-
}
|
|
1205
1654
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1655
|
+
parseFeatureFile(featureFilePath) {
|
|
1656
|
+
try {
|
|
1657
|
+
let id = 0;
|
|
1658
|
+
const uuidFn = () => (++id).toString(16);
|
|
1659
|
+
const builder = new AstBuilder(uuidFn);
|
|
1660
|
+
const matcher = new GherkinClassicTokenMatcher();
|
|
1661
|
+
const parser = new Parser(builder, matcher);
|
|
1662
|
+
const source = readFileSync(featureFilePath, "utf8");
|
|
1663
|
+
const gherkinDocument = parser.parse(source);
|
|
1664
|
+
return gherkinDocument;
|
|
1665
|
+
} catch (e) {
|
|
1666
|
+
this.logger.error(`Error parsing feature file: ${featureFilePath}`);
|
|
1667
|
+
console.log(e);
|
|
1668
|
+
}
|
|
1669
|
+
return {};
|
|
1218
1670
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1671
|
+
|
|
1672
|
+
stopRecordingNetwork(input) {
|
|
1673
|
+
if (this.bvtContext) {
|
|
1674
|
+
this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
async fakeParams(params) {
|
|
1679
|
+
const newFakeParams = {};
|
|
1680
|
+
Object.keys(params).forEach((key) => {
|
|
1681
|
+
if (!params[key].startsWith("{{") || !params[key].endsWith("}}")) {
|
|
1682
|
+
newFakeParams[key] = params[key];
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
try {
|
|
1687
|
+
const value = params[key].substring(2, params[key].length - 2).trim();
|
|
1688
|
+
const faking = value.split("(")[0].split(".");
|
|
1689
|
+
let argument = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
|
|
1690
|
+
argument = isNaN(Number(argument)) || argument === "" ? argument : Number(argument);
|
|
1691
|
+
let fakeFunc = faker;
|
|
1692
|
+
faking.forEach((f) => {
|
|
1693
|
+
fakeFunc = fakeFunc[f];
|
|
1694
|
+
});
|
|
1695
|
+
const newValue = fakeFunc(argument);
|
|
1696
|
+
newFakeParams[key] = newValue;
|
|
1697
|
+
} catch (error) {
|
|
1698
|
+
newFakeParams[key] = params[key];
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
return newFakeParams;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
async getBrowserState() {
|
|
1706
|
+
try {
|
|
1707
|
+
const state = await this.browserEmitter?.getState();
|
|
1708
|
+
this.sendEvent(this.events.browserStateSync, state);
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
this.logger.error("Error getting browser state:", error);
|
|
1711
|
+
this.sendEvent(this.events.browserStateError, {
|
|
1712
|
+
message: "Error getting browser state",
|
|
1713
|
+
code: "GET_STATE_ERROR",
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
async applyClipboardPayload(message) {
|
|
1719
|
+
const payload = message?.data ?? message;
|
|
1720
|
+
|
|
1721
|
+
this.logger.info("[BVTRecorder] applyClipboardPayload called", {
|
|
1722
|
+
hasPayload: !!payload,
|
|
1723
|
+
hasText: !!payload?.text,
|
|
1724
|
+
hasHtml: !!payload?.html,
|
|
1725
|
+
trigger: message?.trigger,
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
if (!payload) {
|
|
1729
|
+
this.logger.warn("[BVTRecorder] No payload provided");
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
try {
|
|
1734
|
+
if (this.browserEmitter && typeof this.browserEmitter.applyClipboardPayload === "function") {
|
|
1735
|
+
this.logger.info("[BVTRecorder] Using RemoteBrowserService to apply clipboard");
|
|
1736
|
+
await this.browserEmitter.applyClipboardPayload(payload);
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
1741
|
+
if (!activePage) {
|
|
1742
|
+
this.logger.warn("[BVTRecorder] No active page available");
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
this.logger.info("[BVTRecorder] Applying clipboard to page", {
|
|
1747
|
+
url: activePage.url(),
|
|
1748
|
+
isClosed: activePage.isClosed(),
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
const result = await activePage.evaluate((clipboardData) => {
|
|
1752
|
+
console.log("[Page] Executing clipboard application", clipboardData);
|
|
1753
|
+
if (typeof window.__bvt_applyClipboardData === "function") {
|
|
1754
|
+
return window.__bvt_applyClipboardData(clipboardData);
|
|
1755
|
+
}
|
|
1756
|
+
console.error("[Page] __bvt_applyClipboardData function not found!");
|
|
1757
|
+
return false;
|
|
1758
|
+
}, payload);
|
|
1759
|
+
|
|
1760
|
+
this.logger.info("[BVTRecorder] Clipboard application result:", result);
|
|
1761
|
+
|
|
1762
|
+
if (!result) {
|
|
1763
|
+
this.logger.warn("[BVTRecorder] Clipboard data not applied successfully");
|
|
1764
|
+
} else {
|
|
1765
|
+
this.logger.info("[BVTRecorder] Clipboard data applied successfully");
|
|
1766
|
+
}
|
|
1767
|
+
} catch (error) {
|
|
1768
|
+
this.logger.error("[BVTRecorder] Error applying clipboard payload", error);
|
|
1769
|
+
this.sendEvent(this.events.clipboardError, {
|
|
1770
|
+
message: "Failed to apply clipboard contents to the remote session",
|
|
1771
|
+
trigger: message?.trigger ?? "paste",
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
hasClipboardPayload(payload) {
|
|
1777
|
+
return Boolean(
|
|
1778
|
+
payload && (payload.text || payload.html || (Array.isArray(payload.files) && payload.files.length > 0))
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
async collectClipboardFromPage(page) {
|
|
1783
|
+
if (!page) {
|
|
1784
|
+
this.logger.warn("[BVTRecorder] No page available to collect clipboard data");
|
|
1785
|
+
return null;
|
|
1786
|
+
}
|
|
1787
|
+
try {
|
|
1788
|
+
await page
|
|
1789
|
+
.context()
|
|
1790
|
+
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
1791
|
+
.catch((error) => {
|
|
1792
|
+
this.logger.warn("[BVTRecorder] Failed to grant clipboard permissions before read", error);
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
const payload = await page.evaluate(async () => {
|
|
1796
|
+
const result = {};
|
|
1797
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
|
1798
|
+
return result;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
const arrayBufferToBase64 = (buffer) => {
|
|
1802
|
+
let binary = "";
|
|
1803
|
+
const bytes = new Uint8Array(buffer);
|
|
1804
|
+
const chunkSize = 0x8000;
|
|
1805
|
+
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
1806
|
+
const chunk = bytes.subarray(index, index + chunkSize);
|
|
1807
|
+
binary += String.fromCharCode(...chunk);
|
|
1808
|
+
}
|
|
1809
|
+
return btoa(binary);
|
|
1810
|
+
};
|
|
1811
|
+
|
|
1812
|
+
const files = [];
|
|
1813
|
+
|
|
1814
|
+
if (typeof navigator.clipboard.read === "function") {
|
|
1815
|
+
try {
|
|
1816
|
+
const items = await navigator.clipboard.read();
|
|
1817
|
+
for (const item of items) {
|
|
1818
|
+
if (item.types.includes("text/html") && !result.html) {
|
|
1819
|
+
const blob = await item.getType("text/html");
|
|
1820
|
+
result.html = await blob.text();
|
|
1821
|
+
}
|
|
1822
|
+
if (item.types.includes("text/plain") && !result.text) {
|
|
1823
|
+
const blob = await item.getType("text/plain");
|
|
1824
|
+
result.text = await blob.text();
|
|
1825
|
+
}
|
|
1826
|
+
for (const type of item.types) {
|
|
1827
|
+
if (type.startsWith("text/")) {
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
try {
|
|
1831
|
+
const blob = await item.getType(type);
|
|
1832
|
+
const buffer = await blob.arrayBuffer();
|
|
1833
|
+
files.push({
|
|
1834
|
+
name: `clipboard-file-${files.length + 1}`,
|
|
1835
|
+
type,
|
|
1836
|
+
lastModified: Date.now(),
|
|
1837
|
+
data: arrayBufferToBase64(buffer),
|
|
1838
|
+
});
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
console.warn("[BVTRecorder] Failed to serialize clipboard blob", { type, error });
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
} catch (error) {
|
|
1845
|
+
console.warn("[BVTRecorder] navigator.clipboard.read failed", error);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
if (!result.text && typeof navigator.clipboard.readText === "function") {
|
|
1850
|
+
try {
|
|
1851
|
+
const text = await navigator.clipboard.readText();
|
|
1852
|
+
if (text) {
|
|
1853
|
+
result.text = text;
|
|
1854
|
+
}
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
console.warn("[BVTRecorder] navigator.clipboard.readText failed", error);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
if (!result.text) {
|
|
1861
|
+
const selection = window.getSelection?.()?.toString?.();
|
|
1862
|
+
if (selection) {
|
|
1863
|
+
result.text = selection;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
if (files.length > 0) {
|
|
1868
|
+
result.files = files;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
return result;
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
return payload;
|
|
1875
|
+
} catch (error) {
|
|
1876
|
+
this.logger.error("[BVTRecorder] Error collecting clipboard payload", error);
|
|
1877
|
+
return null;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
async readClipboardPayload(message) {
|
|
1882
|
+
try {
|
|
1883
|
+
let payload = null;
|
|
1884
|
+
if (this.browserEmitter && typeof this.browserEmitter.readClipboardPayload === "function") {
|
|
1885
|
+
payload = await this.browserEmitter.readClipboardPayload();
|
|
1886
|
+
} else {
|
|
1887
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
1888
|
+
payload = await this.collectClipboardFromPage(activePage);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
if (this.hasClipboardPayload(payload)) {
|
|
1892
|
+
this.logger.info("[BVTRecorder] Remote clipboard payload ready", {
|
|
1893
|
+
hasText: !!payload.text,
|
|
1894
|
+
hasHtml: !!payload.html,
|
|
1895
|
+
files: payload.files?.length ?? 0,
|
|
1896
|
+
});
|
|
1897
|
+
this.sendEvent(this.events.clipboardPush, {
|
|
1898
|
+
data: payload,
|
|
1899
|
+
trigger: message?.trigger ?? "copy",
|
|
1900
|
+
origin: message?.source ?? "browserUI",
|
|
1901
|
+
});
|
|
1902
|
+
return payload;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
this.logger.warn("[BVTRecorder] Remote clipboard payload empty or unavailable");
|
|
1906
|
+
this.sendEvent(this.events.clipboardError, {
|
|
1907
|
+
message: "Remote clipboard is empty",
|
|
1908
|
+
trigger: message?.trigger ?? "copy",
|
|
1909
|
+
});
|
|
1910
|
+
return null;
|
|
1911
|
+
} catch (error) {
|
|
1912
|
+
this.logger.error("[BVTRecorder] Error reading clipboard payload", error);
|
|
1913
|
+
this.sendEvent(this.events.clipboardError, {
|
|
1914
|
+
message: "Failed to read clipboard contents from the remote session",
|
|
1915
|
+
trigger: message?.trigger ?? "copy",
|
|
1916
|
+
details: error instanceof Error ? error.message : String(error),
|
|
1917
|
+
});
|
|
1918
|
+
throw error;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
async injectClipboardIntoPage(page, payload) {
|
|
1923
|
+
if (!page) {
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
try {
|
|
1928
|
+
await page
|
|
1929
|
+
.context()
|
|
1930
|
+
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
1931
|
+
.catch(() => {});
|
|
1932
|
+
await page.evaluate(async (clipboardPayload) => {
|
|
1933
|
+
const toArrayBuffer = (base64) => {
|
|
1934
|
+
if (!base64) {
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
const binaryString = atob(base64);
|
|
1938
|
+
const len = binaryString.length;
|
|
1939
|
+
const bytes = new Uint8Array(len);
|
|
1940
|
+
for (let i = 0; i < len; i += 1) {
|
|
1941
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
1942
|
+
}
|
|
1943
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
1944
|
+
};
|
|
1945
|
+
|
|
1946
|
+
const createFileFromPayload = (filePayload) => {
|
|
1947
|
+
const buffer = toArrayBuffer(filePayload?.data);
|
|
1948
|
+
if (!buffer) {
|
|
1949
|
+
return null;
|
|
1950
|
+
}
|
|
1951
|
+
const name = filePayload?.name || "clipboard-file";
|
|
1952
|
+
const type = filePayload?.type || "application/octet-stream";
|
|
1953
|
+
const lastModified = filePayload?.lastModified ?? Date.now();
|
|
1954
|
+
try {
|
|
1955
|
+
return new File([buffer], name, { type, lastModified });
|
|
1956
|
+
} catch (error) {
|
|
1957
|
+
console.warn("Clipboard bridge could not recreate File object", error);
|
|
1958
|
+
return null;
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
|
|
1962
|
+
let dataTransfer = null;
|
|
1963
|
+
try {
|
|
1964
|
+
dataTransfer = new DataTransfer();
|
|
1965
|
+
} catch (error) {
|
|
1966
|
+
console.warn("Clipboard bridge could not create DataTransfer", error);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
if (dataTransfer) {
|
|
1970
|
+
if (clipboardPayload?.text) {
|
|
1971
|
+
try {
|
|
1972
|
+
dataTransfer.setData("text/plain", clipboardPayload.text);
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
console.warn("Clipboard bridge failed to set text/plain", error);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
if (clipboardPayload?.html) {
|
|
1978
|
+
try {
|
|
1979
|
+
dataTransfer.setData("text/html", clipboardPayload.html);
|
|
1980
|
+
} catch (error) {
|
|
1981
|
+
console.warn("Clipboard bridge failed to set text/html", error);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
if (Array.isArray(clipboardPayload?.files)) {
|
|
1985
|
+
for (const filePayload of clipboardPayload.files) {
|
|
1986
|
+
const file = createFileFromPayload(filePayload);
|
|
1987
|
+
if (file) {
|
|
1988
|
+
try {
|
|
1989
|
+
dataTransfer.items.add(file);
|
|
1990
|
+
} catch (error) {
|
|
1991
|
+
console.warn("Clipboard bridge failed to append file", error);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
let target = document.activeElement || document.body;
|
|
1999
|
+
if (!target) {
|
|
2000
|
+
target = document.body || null;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
let pasteHandled = false;
|
|
2004
|
+
if (dataTransfer && target && typeof target.dispatchEvent === "function") {
|
|
2005
|
+
try {
|
|
2006
|
+
const clipboardEvent = new ClipboardEvent("paste", {
|
|
2007
|
+
clipboardData: dataTransfer,
|
|
2008
|
+
bubbles: true,
|
|
2009
|
+
cancelable: true,
|
|
2010
|
+
});
|
|
2011
|
+
pasteHandled = target.dispatchEvent(clipboardEvent);
|
|
2012
|
+
} catch (error) {
|
|
2013
|
+
console.warn("Clipboard bridge failed to dispatch synthetic paste event", error);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
if (pasteHandled) {
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
const callLegacyExecCommand = (command, value) => {
|
|
2022
|
+
const execCommand = document && document["execCommand"];
|
|
2023
|
+
if (typeof execCommand === "function") {
|
|
2024
|
+
try {
|
|
2025
|
+
return execCommand.call(document, command, false, value);
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
console.warn("Clipboard bridge failed to execute legacy command", error);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
return false;
|
|
2031
|
+
};
|
|
2032
|
+
|
|
2033
|
+
if (clipboardPayload?.html) {
|
|
2034
|
+
const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
|
|
2035
|
+
if (inserted) {
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
try {
|
|
2039
|
+
const selection = window.getSelection?.();
|
|
2040
|
+
if (selection && selection.rangeCount > 0) {
|
|
2041
|
+
const range = selection.getRangeAt(0);
|
|
2042
|
+
range.deleteContents();
|
|
2043
|
+
const fragment = range.createContextualFragment(clipboardPayload.html);
|
|
2044
|
+
range.insertNode(fragment);
|
|
2045
|
+
range.collapse(false);
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
console.warn("Clipboard bridge could not insert HTML via Range APIs", error);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
if (clipboardPayload?.text) {
|
|
2054
|
+
const inserted = callLegacyExecCommand("insertText", clipboardPayload.text);
|
|
2055
|
+
if (inserted) {
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
try {
|
|
2059
|
+
const selection = window.getSelection?.();
|
|
2060
|
+
if (selection && selection.rangeCount > 0) {
|
|
2061
|
+
const range = selection.getRangeAt(0);
|
|
2062
|
+
range.deleteContents();
|
|
2063
|
+
range.insertNode(document.createTextNode(clipboardPayload.text));
|
|
2064
|
+
range.collapse(false);
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
} catch (error) {
|
|
2068
|
+
console.warn("Clipboard bridge could not insert text via Range APIs", error);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
if (clipboardPayload?.text && target && "value" in target) {
|
|
2073
|
+
try {
|
|
2074
|
+
const input = target;
|
|
2075
|
+
const start = input.selectionStart ?? input.value.length ?? 0;
|
|
2076
|
+
const end = input.selectionEnd ?? input.value.length ?? 0;
|
|
2077
|
+
const value = input.value ?? "";
|
|
2078
|
+
const text = clipboardPayload.text;
|
|
2079
|
+
input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
|
|
2080
|
+
const caret = start + text.length;
|
|
2081
|
+
if (typeof input.setSelectionRange === "function") {
|
|
2082
|
+
input.setSelectionRange(caret, caret);
|
|
2083
|
+
}
|
|
2084
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2085
|
+
} catch (error) {
|
|
2086
|
+
console.warn("Clipboard bridge failed to mutate input element", error);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}, payload);
|
|
2090
|
+
} catch (error) {
|
|
2091
|
+
throw error;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
async createTab(url) {
|
|
2096
|
+
try {
|
|
2097
|
+
await this.browserEmitter?.createTab(url);
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
this.logger.error("Error creating tab:", error);
|
|
2100
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2101
|
+
message: "Error creating tab",
|
|
2102
|
+
code: "CREATE_TAB_ERROR",
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
async closeTab(pageId) {
|
|
2108
|
+
try {
|
|
2109
|
+
await this.browserEmitter?.closeTab(pageId);
|
|
2110
|
+
} catch (error) {
|
|
2111
|
+
this.logger.error("Error closing tab:", error);
|
|
2112
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2113
|
+
message: "Error closing tab",
|
|
2114
|
+
code: "CLOSE_TAB_ERROR",
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
async selectTab(pageId) {
|
|
2120
|
+
try {
|
|
2121
|
+
await this.browserEmitter?.selectTab(pageId);
|
|
2122
|
+
} catch (error) {
|
|
2123
|
+
this.logger.error("Error selecting tab:", error);
|
|
2124
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2125
|
+
message: "Error selecting tab",
|
|
2126
|
+
code: "SELECT_TAB_ERROR",
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
async navigateTab({ pageId, url }) {
|
|
2132
|
+
try {
|
|
2133
|
+
if (!pageId || !url) {
|
|
2134
|
+
this.logger.error("navigateTab called without pageId or url", { pageId, url });
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
await this.browserEmitter?.navigateTab(pageId, url);
|
|
2138
|
+
} catch (error) {
|
|
2139
|
+
this.logger.error("Error navigating tab:", error);
|
|
2140
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2141
|
+
message: "Error navigating tab",
|
|
2142
|
+
code: "NAVIGATE_TAB_ERROR",
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
async reloadTab(pageId) {
|
|
2148
|
+
try {
|
|
2149
|
+
await this.browserEmitter?.reloadTab(pageId);
|
|
2150
|
+
} catch (error) {
|
|
2151
|
+
this.logger.error("Error reloading tab:", error);
|
|
2152
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2153
|
+
message: "Error reloading tab",
|
|
2154
|
+
code: "RELOAD_TAB_ERROR",
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
async goBack(pageId) {
|
|
2160
|
+
try {
|
|
2161
|
+
await this.browserEmitter?.goBack(pageId);
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
this.logger.error("Error navigating back:", error);
|
|
2164
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2165
|
+
message: "Error navigating back",
|
|
2166
|
+
code: "GO_BACK_ERROR",
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
async goForward(pageId) {
|
|
2172
|
+
try {
|
|
2173
|
+
await this.browserEmitter?.goForward(pageId);
|
|
2174
|
+
} catch (error) {
|
|
2175
|
+
this.logger.error("Error navigating forward:", error);
|
|
2176
|
+
this.sendEvent(this.events.browserStateError, {
|
|
2177
|
+
message: "Error navigating forward",
|
|
2178
|
+
code: "GO_FORWARD_ERROR",
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
}
|