@dev-blinq/cucumber_client 1.0.1710-dev → 1.0.1712-dev
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/client/code_gen/code_inversion.js +21 -11
- package/bin/client/code_gen/function_signature.js +4 -0
- package/bin/client/code_gen/index.js +4 -0
- package/bin/client/code_gen/page_reflection.js +10 -5
- package/bin/client/code_gen/playwright_codeget.js +20 -17
- package/bin/client/recorderv3/bvt_init.js +286 -304
- package/bin/client/recorderv3/bvt_recorder.js +1497 -2393
- package/bin/client/recorderv3/wbr_entry.js +1 -1
- package/bin/client/types/locators.js +2 -0
- package/bin/client/utils/socket_logger.js +14 -1
- package/package.json +2 -2
|
@@ -5,17 +5,10 @@ import { rm } from "fs/promises";
|
|
|
5
5
|
import path from "path";
|
|
6
6
|
import url from "url";
|
|
7
7
|
import { getImplementedSteps, parseRouteFiles } from "./implemented_steps.js";
|
|
8
|
-
import { NamesService,
|
|
8
|
+
import { NamesService, PublishService } from "./services.js";
|
|
9
9
|
import { BVTStepRunner } from "./step_runner.js";
|
|
10
10
|
import { readFile, writeFile } from "fs/promises";
|
|
11
|
-
import {
|
|
12
|
-
loadStepDefinitions,
|
|
13
|
-
getCommandsForImplementedStep,
|
|
14
|
-
getCodePage,
|
|
15
|
-
getCucumberStep,
|
|
16
|
-
_toRecordingStep,
|
|
17
|
-
toMethodName,
|
|
18
|
-
} from "./step_utils.js";
|
|
11
|
+
import { loadStepDefinitions, getCommandsForImplementedStep, getCodePage, getCucumberStep, _toRecordingStep, toMethodName, } from "./step_utils.js";
|
|
19
12
|
import { parseStepTextParameters } from "../cucumber/utils.js";
|
|
20
13
|
import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
|
|
21
14
|
import chokidar from "chokidar";
|
|
@@ -28,462 +21,154 @@ import { chromium } from "playwright-core";
|
|
|
28
21
|
import { axiosClient } from "../utils/axiosClient.js";
|
|
29
22
|
import { _generateCodeFromCommand } from "../code_gen/playwright_codeget.js";
|
|
30
23
|
import { Recording } from "../recording.js";
|
|
31
|
-
|
|
32
24
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
33
|
-
|
|
34
25
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
35
|
-
|
|
36
|
-
const clipboardBridgeScript = `
|
|
37
|
-
;(() => {
|
|
38
|
-
if (window.__bvtRecorderClipboardBridgeInitialized) {
|
|
39
|
-
console.log('[ClipboardBridge] Already initialized, skipping');
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
window.__bvtRecorderClipboardBridgeInitialized = true;
|
|
43
|
-
console.log('[ClipboardBridge] Initializing clipboard bridge');
|
|
44
|
-
|
|
45
|
-
const emitPayload = (payload, attempt = 0) => {
|
|
46
|
-
const reporter = window.__bvt_reportClipboard;
|
|
47
|
-
if (typeof reporter === "function") {
|
|
48
|
-
try {
|
|
49
|
-
console.log('[ClipboardBridge] Reporting clipboard payload:', payload);
|
|
50
|
-
reporter(payload);
|
|
51
|
-
} catch (error) {
|
|
52
|
-
console.warn("[ClipboardBridge] Failed to report payload", error);
|
|
53
|
-
}
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (attempt < 5) {
|
|
57
|
-
console.log('[ClipboardBridge] Reporter not ready, retrying...', attempt);
|
|
58
|
-
setTimeout(() => emitPayload(payload, attempt + 1), 50 * (attempt + 1));
|
|
59
|
-
} else {
|
|
60
|
-
console.warn('[ClipboardBridge] Reporter never became available');
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const fileToBase64 = (file) => {
|
|
65
|
-
return new Promise((resolve) => {
|
|
66
|
-
try {
|
|
67
|
-
const reader = new FileReader();
|
|
68
|
-
reader.onload = () => {
|
|
69
|
-
const { result } = reader;
|
|
70
|
-
if (typeof result === "string") {
|
|
71
|
-
const index = result.indexOf("base64,");
|
|
72
|
-
resolve(index !== -1 ? result.substring(index + 7) : result);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (result instanceof ArrayBuffer) {
|
|
76
|
-
const bytes = new Uint8Array(result);
|
|
77
|
-
let binary = "";
|
|
78
|
-
const chunk = 0x8000;
|
|
79
|
-
for (let i = 0; i < bytes.length; i += chunk) {
|
|
80
|
-
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
81
|
-
}
|
|
82
|
-
resolve(btoa(binary));
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
resolve(null);
|
|
86
|
-
};
|
|
87
|
-
reader.onerror = () => resolve(null);
|
|
88
|
-
reader.readAsDataURL(file);
|
|
89
|
-
} catch (error) {
|
|
90
|
-
console.warn("[ClipboardBridge] Failed to serialize file", error);
|
|
91
|
-
resolve(null);
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const handleClipboardEvent = async (event) => {
|
|
97
|
-
try {
|
|
98
|
-
console.log('[ClipboardBridge] Handling clipboard event:', event.type);
|
|
99
|
-
const payload = { trigger: event.type };
|
|
100
|
-
const clipboardData = event.clipboardData;
|
|
101
|
-
|
|
102
|
-
if (clipboardData) {
|
|
103
|
-
try {
|
|
104
|
-
const text = clipboardData.getData("text/plain");
|
|
105
|
-
if (text) {
|
|
106
|
-
payload.text = text;
|
|
107
|
-
console.log('[ClipboardBridge] Captured text:', text.substring(0, 50));
|
|
108
|
-
}
|
|
109
|
-
} catch (error) {
|
|
110
|
-
console.warn("[ClipboardBridge] Could not read text/plain", error);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
const html = clipboardData.getData("text/html");
|
|
115
|
-
if (html) {
|
|
116
|
-
payload.html = html;
|
|
117
|
-
console.log('[ClipboardBridge] Captured HTML:', html.substring(0, 50));
|
|
118
|
-
}
|
|
119
|
-
} catch (error) {
|
|
120
|
-
console.warn("[ClipboardBridge] Could not read text/html", error);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const files = clipboardData.files;
|
|
124
|
-
if (files && files.length > 0) {
|
|
125
|
-
console.log('[ClipboardBridge] Processing files:', files.length);
|
|
126
|
-
const serialized = [];
|
|
127
|
-
for (const file of files) {
|
|
128
|
-
const data = await fileToBase64(file);
|
|
129
|
-
if (data) {
|
|
130
|
-
serialized.push({
|
|
131
|
-
name: file.name,
|
|
132
|
-
type: file.type,
|
|
133
|
-
lastModified: file.lastModified,
|
|
134
|
-
data,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (serialized.length > 0) {
|
|
139
|
-
payload.files = serialized;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!payload.text) {
|
|
145
|
-
try {
|
|
146
|
-
const selection = window.getSelection?.();
|
|
147
|
-
const selectionText = selection?.toString?.();
|
|
148
|
-
if (selectionText) {
|
|
149
|
-
payload.text = selectionText;
|
|
150
|
-
console.log('[ClipboardBridge] Using selection text:', selectionText.substring(0, 50));
|
|
151
|
-
}
|
|
152
|
-
} catch {
|
|
153
|
-
// Ignore selection access errors.
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
emitPayload(payload);
|
|
158
|
-
} catch (error) {
|
|
159
|
-
console.warn("[ClipboardBridge] Could not process event", error);
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
// NEW: Function to apply clipboard data to the page
|
|
164
|
-
window.__bvt_applyClipboardData = (payload) => {
|
|
165
|
-
console.log('[ClipboardBridge] Applying clipboard data:', payload);
|
|
166
|
-
|
|
167
|
-
if (!payload) {
|
|
168
|
-
console.warn('[ClipboardBridge] No payload provided');
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
// Create DataTransfer object
|
|
174
|
-
let dataTransfer = null;
|
|
175
|
-
try {
|
|
176
|
-
dataTransfer = new DataTransfer();
|
|
177
|
-
console.log('[ClipboardBridge] DataTransfer created');
|
|
178
|
-
} catch (error) {
|
|
179
|
-
console.warn('[ClipboardBridge] Could not create DataTransfer', error);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (dataTransfer) {
|
|
183
|
-
if (payload.text) {
|
|
184
|
-
try {
|
|
185
|
-
dataTransfer.setData("text/plain", payload.text);
|
|
186
|
-
console.log('[ClipboardBridge] Set text/plain:', payload.text.substring(0, 50));
|
|
187
|
-
} catch (error) {
|
|
188
|
-
console.warn('[ClipboardBridge] Failed to set text/plain', error);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (payload.html) {
|
|
192
|
-
try {
|
|
193
|
-
dataTransfer.setData("text/html", payload.html);
|
|
194
|
-
console.log('[ClipboardBridge] Set text/html:', payload.html.substring(0, 50));
|
|
195
|
-
} catch (error) {
|
|
196
|
-
console.warn('[ClipboardBridge] Failed to set text/html', error);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Get target element
|
|
202
|
-
let target = document.activeElement || document.body;
|
|
203
|
-
console.log('[ClipboardBridge] Target element:', {
|
|
204
|
-
tagName: target.tagName,
|
|
205
|
-
type: target.type,
|
|
206
|
-
isContentEditable: target.isContentEditable,
|
|
207
|
-
id: target.id,
|
|
208
|
-
className: target.className
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// Try synthetic paste event first
|
|
212
|
-
let pasteHandled = false;
|
|
213
|
-
if (dataTransfer && target && typeof target.dispatchEvent === "function") {
|
|
214
|
-
try {
|
|
215
|
-
const pasteEvent = new ClipboardEvent("paste", {
|
|
216
|
-
clipboardData: dataTransfer,
|
|
217
|
-
bubbles: true,
|
|
218
|
-
cancelable: true,
|
|
219
|
-
});
|
|
220
|
-
pasteHandled = target.dispatchEvent(pasteEvent);
|
|
221
|
-
console.log('[ClipboardBridge] Paste event dispatched, handled:', pasteHandled);
|
|
222
|
-
} catch (error) {
|
|
223
|
-
console.warn('[ClipboardBridge] Failed to dispatch paste event', error);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
console.log('[ClipboardBridge] Paste event not handled, trying fallback methods');
|
|
229
|
-
|
|
230
|
-
// Fallback: Try execCommand with HTML first (for contenteditable)
|
|
231
|
-
if (payload.html && target.isContentEditable) {
|
|
232
|
-
console.log('[ClipboardBridge] Trying execCommand insertHTML');
|
|
233
|
-
try {
|
|
234
|
-
const inserted = document.execCommand('insertHTML', false, payload.html);
|
|
235
|
-
if (inserted) {
|
|
236
|
-
console.log('[ClipboardBridge] Successfully inserted HTML via execCommand');
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
} catch (error) {
|
|
240
|
-
console.warn('[ClipboardBridge] execCommand insertHTML failed', error);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Try Range API for HTML
|
|
244
|
-
console.log('[ClipboardBridge] Trying Range API for HTML');
|
|
245
|
-
try {
|
|
246
|
-
const selection = window.getSelection?.();
|
|
247
|
-
if (selection && selection.rangeCount > 0) {
|
|
248
|
-
const range = selection.getRangeAt(0);
|
|
249
|
-
range.deleteContents();
|
|
250
|
-
const fragment = range.createContextualFragment(payload.html);
|
|
251
|
-
range.insertNode(fragment);
|
|
252
|
-
range.collapse(false);
|
|
253
|
-
console.log('[ClipboardBridge] Successfully inserted HTML via Range API');
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
} catch (error) {
|
|
257
|
-
console.warn('[ClipboardBridge] Range API HTML insertion failed', error);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Fallback: Try execCommand with text
|
|
262
|
-
if (payload.text) {
|
|
263
|
-
console.log('[ClipboardBridge] Trying execCommand insertText');
|
|
264
|
-
try {
|
|
265
|
-
const inserted = document.execCommand('insertText', false, payload.text);
|
|
266
|
-
if (inserted) {
|
|
267
|
-
console.log('[ClipboardBridge] Successfully inserted text via execCommand');
|
|
268
|
-
return true;
|
|
269
|
-
}
|
|
270
|
-
} catch (error) {
|
|
271
|
-
console.warn('[ClipboardBridge] execCommand insertText failed', error);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Try Range API for text
|
|
275
|
-
if (target.isContentEditable) {
|
|
276
|
-
console.log('[ClipboardBridge] Trying Range API for text');
|
|
277
|
-
try {
|
|
278
|
-
const selection = window.getSelection?.();
|
|
279
|
-
if (selection && selection.rangeCount > 0) {
|
|
280
|
-
const range = selection.getRangeAt(0);
|
|
281
|
-
range.deleteContents();
|
|
282
|
-
range.insertNode(document.createTextNode(payload.text));
|
|
283
|
-
range.collapse(false);
|
|
284
|
-
console.log('[ClipboardBridge] Successfully inserted text via Range API');
|
|
285
|
-
return true;
|
|
286
|
-
}
|
|
287
|
-
} catch (error) {
|
|
288
|
-
console.warn('[ClipboardBridge] Range API text insertion failed', error);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Last resort: Direct value assignment for input/textarea
|
|
293
|
-
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
|
|
294
|
-
console.log('[ClipboardBridge] Trying direct value assignment');
|
|
295
|
-
try {
|
|
296
|
-
const start = target.selectionStart ?? target.value.length ?? 0;
|
|
297
|
-
const end = target.selectionEnd ?? target.value.length ?? 0;
|
|
298
|
-
const value = target.value ?? "";
|
|
299
|
-
const text = payload.text;
|
|
300
|
-
target.value = value.slice(0, start) + text + value.slice(end);
|
|
301
|
-
const caret = start + text.length;
|
|
302
|
-
if (typeof target.setSelectionRange === 'function') {
|
|
303
|
-
target.setSelectionRange(caret, caret);
|
|
304
|
-
}
|
|
305
|
-
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
306
|
-
console.log('[ClipboardBridge] Successfully set value directly');
|
|
307
|
-
return true;
|
|
308
|
-
} catch (error) {
|
|
309
|
-
console.warn('[ClipboardBridge] Direct value assignment failed', error);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
console.warn('[ClipboardBridge] All paste methods failed');
|
|
315
|
-
return false;
|
|
316
|
-
} catch (error) {
|
|
317
|
-
console.error('[ClipboardBridge] Error applying clipboard data:', error);
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
// Set up event listeners for copy/cut
|
|
323
|
-
document.addEventListener(
|
|
324
|
-
"copy",
|
|
325
|
-
(event) => {
|
|
326
|
-
void handleClipboardEvent(event);
|
|
327
|
-
},
|
|
328
|
-
true
|
|
329
|
-
);
|
|
330
|
-
document.addEventListener(
|
|
331
|
-
"cut",
|
|
332
|
-
(event) => {
|
|
333
|
-
void handleClipboardEvent(event);
|
|
334
|
-
},
|
|
335
|
-
true
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
console.log('[ClipboardBridge] Clipboard bridge initialized successfully');
|
|
339
|
-
})();
|
|
340
|
-
`;
|
|
341
|
-
|
|
342
26
|
export function getInitScript(config, options) {
|
|
343
|
-
|
|
27
|
+
const preScript = `
|
|
344
28
|
window.__bvt_Recorder_config = ${JSON.stringify(config ?? null)};
|
|
345
29
|
window.__PW_options = ${JSON.stringify(options ?? null)};
|
|
346
30
|
`;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
"utf8"
|
|
350
|
-
);
|
|
351
|
-
return preScript + recorderScript + clipboardBridgeScript;
|
|
31
|
+
const recorderScript = readFileSync(path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"), "utf8");
|
|
32
|
+
return preScript + recorderScript;
|
|
352
33
|
}
|
|
353
|
-
|
|
354
34
|
async function evaluate(frame, script) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
await evaluate(
|
|
361
|
-
|
|
35
|
+
if (frame.isDetached())
|
|
36
|
+
return;
|
|
37
|
+
const url = frame.url();
|
|
38
|
+
if (url === "" || url === "about:blank")
|
|
39
|
+
return;
|
|
40
|
+
await frame.evaluate(script);
|
|
41
|
+
for (const childFrame of frame.childFrames()) {
|
|
42
|
+
await evaluate(childFrame, script);
|
|
43
|
+
}
|
|
362
44
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
45
|
+
async function findNestedFrameSelector(frame, obj = {}) {
|
|
46
|
+
try {
|
|
47
|
+
const parent = frame.parentFrame();
|
|
48
|
+
if (!parent)
|
|
49
|
+
return { children: obj };
|
|
50
|
+
const frameElement = await frame.frameElement();
|
|
51
|
+
if (!frameElement)
|
|
52
|
+
return;
|
|
53
|
+
const selectors = await frameElement.evaluate((element) => {
|
|
54
|
+
const recorder = window.__bvt_Recorder;
|
|
55
|
+
return recorder.locatorGenerator.getElementLocators(element, { excludeText: true }).locators;
|
|
56
|
+
});
|
|
57
|
+
return findNestedFrameSelector(parent, { children: obj, selectors });
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
socketLogger.error(`Error in script evaluation: ${getErrorMessage(e)}`, undefined, "findNestedFrameSelector");
|
|
61
|
+
}
|
|
377
62
|
}
|
|
378
63
|
const transformFillAction = (action, el) => {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
64
|
+
if (el.tagName.toLowerCase() === "input") {
|
|
65
|
+
switch (el.type) {
|
|
66
|
+
case "date":
|
|
67
|
+
case "datetime-local":
|
|
68
|
+
case "month":
|
|
69
|
+
case "time":
|
|
70
|
+
case "week":
|
|
71
|
+
case "range":
|
|
72
|
+
case "color":
|
|
73
|
+
return {
|
|
74
|
+
type: "set_input",
|
|
75
|
+
value: action.text,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
392
78
|
}
|
|
393
|
-
}
|
|
394
|
-
return {
|
|
395
|
-
type: "fill_element",
|
|
396
|
-
value: action.text,
|
|
397
|
-
};
|
|
398
|
-
};
|
|
399
|
-
const transformClickAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode, isSnapshot) => {
|
|
400
|
-
if (isInHoverMode) {
|
|
401
|
-
return {
|
|
402
|
-
type: "hover_element",
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
if (isVerify) {
|
|
406
|
-
return {
|
|
407
|
-
type: "verify_page_contains_text",
|
|
408
|
-
value: el.value ?? el.text,
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
if (isPopupCloseClick) {
|
|
412
79
|
return {
|
|
413
|
-
|
|
80
|
+
type: "fill_element",
|
|
81
|
+
value: action.text,
|
|
414
82
|
};
|
|
415
|
-
|
|
416
|
-
|
|
83
|
+
};
|
|
84
|
+
const transformClickAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode, isSnapshot) => {
|
|
85
|
+
if (isInHoverMode) {
|
|
86
|
+
return {
|
|
87
|
+
type: "hover_element",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (isVerify) {
|
|
91
|
+
return {
|
|
92
|
+
type: "verify_page_contains_text",
|
|
93
|
+
value: el.value ?? el.text,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (isPopupCloseClick) {
|
|
97
|
+
return {
|
|
98
|
+
type: "popup_close",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (isSnapshot) {
|
|
102
|
+
return {
|
|
103
|
+
type: "snapshot_element",
|
|
104
|
+
value: action.value,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
417
107
|
return {
|
|
418
|
-
|
|
419
|
-
value: action.value,
|
|
108
|
+
type: "click_element",
|
|
420
109
|
};
|
|
421
|
-
}
|
|
422
|
-
return {
|
|
423
|
-
type: "click_element",
|
|
424
|
-
};
|
|
425
110
|
};
|
|
426
111
|
const transformAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode, isSnapshot) => {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
}
|
|
112
|
+
switch (action.name) {
|
|
113
|
+
case "click":
|
|
114
|
+
return transformClickAction(action, el, isVerify, isPopupCloseClick, isInHoverMode, isSnapshot);
|
|
115
|
+
case "fill": {
|
|
116
|
+
return transformFillAction(action, el);
|
|
117
|
+
}
|
|
118
|
+
case "select": {
|
|
119
|
+
return {
|
|
120
|
+
type: "select_combobox",
|
|
121
|
+
value: action.options?.[0] ?? "",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
case "check": {
|
|
125
|
+
return {
|
|
126
|
+
type: "check_element",
|
|
127
|
+
check: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
case "uncheck": {
|
|
131
|
+
return {
|
|
132
|
+
type: "check_element",
|
|
133
|
+
check: false,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
case "assertText": {
|
|
137
|
+
return {
|
|
138
|
+
type: "verify_page_contains_text",
|
|
139
|
+
value: action.text,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
case "press": {
|
|
143
|
+
return {
|
|
144
|
+
type: "press_key",
|
|
145
|
+
value: action.key,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
case "setInputFiles": {
|
|
149
|
+
return {
|
|
150
|
+
type: "set_input_files",
|
|
151
|
+
files: action.files,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
default: {
|
|
155
|
+
socketLogger.error(`Action not supported: ${action.name}`);
|
|
156
|
+
console.log("action not supported", action);
|
|
157
|
+
throw new Error("action not supported");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
476
160
|
};
|
|
477
161
|
const diffPaths = (currentPath, newPath) => {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
162
|
+
const currentDomain = new URL(currentPath).hostname;
|
|
163
|
+
const newDomain = new URL(newPath).hostname;
|
|
164
|
+
if (currentDomain !== newDomain) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const currentRoute = new URL(currentPath).pathname;
|
|
169
|
+
const newRoute = new URL(newPath).pathname;
|
|
170
|
+
return currentRoute !== newRoute;
|
|
171
|
+
}
|
|
487
172
|
};
|
|
488
173
|
/**
|
|
489
174
|
* @typedef {Object} BVTRecorderInput
|
|
@@ -494,1994 +179,1413 @@ const diffPaths = (currentPath, newPath) => {
|
|
|
494
179
|
* @property {Object} logger
|
|
495
180
|
*/
|
|
496
181
|
export class BVTRecorder {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
};
|
|
551
|
-
bindings = {
|
|
552
|
-
__bvt_recordCommand: async ({ frame, page, context }, event) => {
|
|
553
|
-
this.#activeFrame = frame;
|
|
554
|
-
const nestFrmLoc = await findNestedFrameSelector(frame);
|
|
555
|
-
this.logger.info(`Time taken for action: ${event.statistics.time}`);
|
|
556
|
-
await this.onAction({ ...event, nestFrmLoc });
|
|
557
|
-
},
|
|
558
|
-
__bvt_getMode: async () => {
|
|
559
|
-
return this.#mode;
|
|
560
|
-
},
|
|
561
|
-
__bvt_setMode: async (src, mode) => {
|
|
562
|
-
await this.setMode(mode);
|
|
563
|
-
},
|
|
564
|
-
__bvt_revertMode: async () => {
|
|
565
|
-
await this.revertMode();
|
|
566
|
-
},
|
|
567
|
-
__bvt_recordPageClose: async ({ page }) => {
|
|
568
|
-
this.pageSet.delete(page);
|
|
569
|
-
},
|
|
570
|
-
__bvt_closePopups: async () => {
|
|
571
|
-
await this.onClosePopup();
|
|
572
|
-
},
|
|
573
|
-
__bvt_log: async (src, message) => {
|
|
574
|
-
this.logger.info(`Inside Browser: ${message}`);
|
|
575
|
-
},
|
|
576
|
-
__bvt_getObject: (_src, obj) => {
|
|
577
|
-
this.processObject(obj);
|
|
578
|
-
},
|
|
579
|
-
__bvt_reportClipboard: async ({ page }, payload) => {
|
|
580
|
-
try {
|
|
581
|
-
if (!payload) {
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
585
|
-
if (activePage && activePage !== page) {
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
const pageUrl = typeof page?.url === "function" ? page.url() : null;
|
|
589
|
-
this.sendEvent(this.events.clipboardPush, {
|
|
590
|
-
data: payload,
|
|
591
|
-
trigger: payload?.trigger ?? "copy",
|
|
592
|
-
pageUrl,
|
|
593
|
-
});
|
|
594
|
-
} catch (error) {
|
|
595
|
-
this.logger.error("Error forwarding clipboard payload from page", error);
|
|
596
|
-
}
|
|
597
|
-
},
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
getSnapshot = async (attr) => {
|
|
601
|
-
const selector = `[__bvt_snapshot="${attr}"]`;
|
|
602
|
-
const locator = await this.web.page.locator(selector);
|
|
603
|
-
const snapshot = await locator.ariaSnapshot();
|
|
604
|
-
return snapshot;
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
processObject = async ({ type, action, value }) => {
|
|
608
|
-
switch (type) {
|
|
609
|
-
case "snapshot-element": {
|
|
610
|
-
if (action === "get-template") {
|
|
611
|
-
return true;
|
|
612
|
-
}
|
|
613
|
-
break;
|
|
614
|
-
}
|
|
615
|
-
default: {
|
|
616
|
-
console.log("Unknown object type", type);
|
|
617
|
-
break;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
};
|
|
621
|
-
|
|
622
|
-
getPWScript() {
|
|
623
|
-
const pwFolder = path.join(__dirname, "..", "..", "assets", "preload", "pw_utils");
|
|
624
|
-
const result = [];
|
|
625
|
-
for (const script of readdirSync(pwFolder)) {
|
|
626
|
-
const path = path.join(pwFolder, script);
|
|
627
|
-
const content = readFileSync(path, "utf8");
|
|
628
|
-
result.push(content);
|
|
629
|
-
}
|
|
630
|
-
return result;
|
|
631
|
-
}
|
|
632
|
-
getRecorderScripts() {
|
|
633
|
-
const recorderFolder = path.join(__dirname, "..", "..", "assets", "preload", "recorder");
|
|
634
|
-
const result = [];
|
|
635
|
-
for (const script of readdirSync(recorderFolder)) {
|
|
636
|
-
const path = path.join(recorderFolder, script);
|
|
637
|
-
const content = readFileSync(path, "utf8");
|
|
638
|
-
result.push(content);
|
|
639
|
-
}
|
|
640
|
-
return result;
|
|
641
|
-
}
|
|
642
|
-
getInitScripts(config) {
|
|
643
|
-
return getInitScript(config, {
|
|
644
|
-
sdkLanguage: "javascript",
|
|
645
|
-
testIdAttributeName: "blinq-test-id",
|
|
646
|
-
stableRafCount: 0,
|
|
647
|
-
browserName: this.browser?.browserType().name(),
|
|
648
|
-
inputFileRoleTextbox: false,
|
|
649
|
-
customEngines: [],
|
|
650
|
-
isUnderTest: true,
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
async _initBrowser({ url }) {
|
|
655
|
-
if (process.env.CDP_LISTEN_PORT === undefined) {
|
|
656
|
-
this.#remoteDebuggerPort = await findAvailablePort();
|
|
657
|
-
process.env.CDP_LISTEN_PORT = this.#remoteDebuggerPort;
|
|
658
|
-
} else {
|
|
659
|
-
this.#remoteDebuggerPort = process.env.CDP_LISTEN_PORT;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
|
|
663
|
-
this.world = { attach: () => { } };
|
|
664
|
-
|
|
665
|
-
const ai_config_file = path.join(this.projectDir, "ai_config.json");
|
|
666
|
-
let ai_config = {};
|
|
667
|
-
if (existsSync(ai_config_file)) {
|
|
668
|
-
try {
|
|
669
|
-
ai_config = JSON.parse(readFileSync(ai_config_file, "utf8"));
|
|
670
|
-
} catch (error) {
|
|
671
|
-
this.logger.error("Error reading ai_config.json", error);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
this.config = ai_config;
|
|
675
|
-
const initScripts = {
|
|
676
|
-
// recorderCjs: injectedScriptSource,
|
|
677
|
-
scripts: [
|
|
678
|
-
this.getInitScripts(ai_config),
|
|
679
|
-
`\ndelete Object.getPrototypeOf(navigator).webdriver;${process.env.WINDOW_DEBUGGER ? "window.debug=true;\n" : ""}`,
|
|
680
|
-
],
|
|
681
|
-
};
|
|
682
|
-
|
|
683
|
-
const scenario = { pickle: this.scenarioDoc };
|
|
684
|
-
const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName, scenario);
|
|
685
|
-
this.bvtContext = bvtContext;
|
|
686
|
-
this.stepRunner = new BVTStepRunner({
|
|
687
|
-
projectDir: this.projectDir,
|
|
688
|
-
sendExecutionStatus: (data) => {
|
|
689
|
-
if (data && data.type) {
|
|
690
|
-
switch (data.type) {
|
|
691
|
-
case "cmdExecutionStart":
|
|
692
|
-
this.sendEvent(this.events.cmdExecutionStart, data);
|
|
693
|
-
break;
|
|
694
|
-
case "cmdExecutionSuccess":
|
|
695
|
-
this.sendEvent(this.events.cmdExecutionSuccess, data);
|
|
696
|
-
break;
|
|
697
|
-
case "cmdExecutionError":
|
|
698
|
-
this.sendEvent(this.events.cmdExecutionError, data);
|
|
699
|
-
break;
|
|
700
|
-
case "interceptResults":
|
|
701
|
-
this.sendEvent(this.events.interceptResults, data);
|
|
702
|
-
break;
|
|
703
|
-
default:
|
|
704
|
-
break;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
},
|
|
708
|
-
bvtContext: this.bvtContext,
|
|
709
|
-
});
|
|
710
|
-
this.context = bvtContext.playContext;
|
|
711
|
-
this.web = bvtContext.stable || bvtContext.web;
|
|
712
|
-
this.web.tryAllStrategies = true;
|
|
713
|
-
this.page = bvtContext.page;
|
|
714
|
-
this.pageSet.add(this.page);
|
|
715
|
-
this.lastKnownUrlPath = this._updateUrlPath();
|
|
716
|
-
const browser = await this.context.browser();
|
|
717
|
-
this.browser = browser;
|
|
718
|
-
|
|
719
|
-
// add bindings
|
|
720
|
-
for (const [name, handler] of Object.entries(this.bindings)) {
|
|
721
|
-
await this.context.exposeBinding(name, handler);
|
|
722
|
-
}
|
|
723
|
-
this._watchTestData();
|
|
724
|
-
this.web.onRestoreSaveState = async (url) => {
|
|
725
|
-
await this._initBrowser({ url });
|
|
726
|
-
this._addPagelisteners(this.context);
|
|
727
|
-
this._addFrameNavigateListener(this.page);
|
|
728
|
-
};
|
|
729
|
-
|
|
730
|
-
// create a second browser for locator generation
|
|
731
|
-
this.backgroundBrowser = await chromium.launch({
|
|
732
|
-
headless: true,
|
|
733
|
-
});
|
|
734
|
-
this.backgroundContext = await this.backgroundBrowser.newContext({});
|
|
735
|
-
await this.backgroundContext.addInitScript({ content: this.getInitScripts(this.config) });
|
|
736
|
-
await this.backgroundContext.newPage();
|
|
737
|
-
}
|
|
738
|
-
async onClosePopup() {
|
|
739
|
-
// console.log("close popups");
|
|
740
|
-
await this.bvtContext.web.closeUnexpectedPopups();
|
|
741
|
-
}
|
|
742
|
-
async evaluateInAllFrames(context, script) {
|
|
743
|
-
// retry 3 times
|
|
744
|
-
for (let i = 0; i < 3; i++) {
|
|
745
|
-
try {
|
|
746
|
-
for (const page of context.pages()) {
|
|
747
|
-
await evaluate(page.mainFrame(), script);
|
|
748
|
-
}
|
|
749
|
-
return;
|
|
750
|
-
} catch (error) {
|
|
751
|
-
// console.error("Error evaluting in context:", error);
|
|
752
|
-
this.logger.error("Error evaluating in context:", error);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
getMode() {
|
|
758
|
-
// console.log("getMode", this.#mode);
|
|
759
|
-
this.logger.info("Current mode:", this.#mode);
|
|
760
|
-
return this.#mode;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
async setMode(mode) {
|
|
764
|
-
await this.evaluateInAllFrames(this.context, `window.__bvt_Recorder.mode = "${mode}";`);
|
|
765
|
-
this.#previousMode = this.#mode;
|
|
766
|
-
this.#mode = mode;
|
|
767
|
-
}
|
|
768
|
-
async revertMode() {
|
|
769
|
-
await this.setMode(this.#previousMode);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
async _openTab({ url }) {
|
|
773
|
-
// add listeners for new pages
|
|
774
|
-
this._addPagelisteners(this.context);
|
|
775
|
-
|
|
776
|
-
await this.page.goto(url, {
|
|
777
|
-
waitUntil: "domcontentloaded",
|
|
778
|
-
timeout: this.config.page_timeout ?? 60_000,
|
|
779
|
-
});
|
|
780
|
-
// add listener for frame navigation on current tab
|
|
781
|
-
this._addFrameNavigateListener(this.page);
|
|
782
|
-
|
|
783
|
-
// eval init script on current tab
|
|
784
|
-
// await this._initPage(this.page);
|
|
785
|
-
this.#currentURL = url;
|
|
786
|
-
|
|
787
|
-
await this.page.dispatchEvent("html", "scroll");
|
|
788
|
-
await delay(1000);
|
|
789
|
-
}
|
|
790
|
-
_addFrameNavigateListener(page) {
|
|
791
|
-
page.on("close", () => {
|
|
792
|
-
try {
|
|
793
|
-
if (!this.pageSet.has(page)) return;
|
|
794
|
-
// console.log(this.context.pages().length);
|
|
795
|
-
if (this.context.pages().length > 0) {
|
|
796
|
-
this.sendEvent(this.events.onPageClose);
|
|
797
|
-
} else {
|
|
798
|
-
// closed all tabs
|
|
799
|
-
this.sendEvent(this.events.onBrowserClose);
|
|
800
|
-
}
|
|
801
|
-
} catch (error) {
|
|
802
|
-
this.logger.error("Error in page close event");
|
|
803
|
-
this.logger.error(error);
|
|
804
|
-
console.error("Error in page close event");
|
|
805
|
-
console.error(error);
|
|
806
|
-
}
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
page.on("framenavigated", async (frame) => {
|
|
810
|
-
try {
|
|
811
|
-
if (frame !== page.mainFrame()) return;
|
|
812
|
-
this.handlePageTransition();
|
|
813
|
-
} catch (error) {
|
|
814
|
-
this.logger.error("Error in handlePageTransition event");
|
|
815
|
-
this.logger.error(error);
|
|
816
|
-
console.error("Error in handlePageTransition event");
|
|
817
|
-
console.error(error);
|
|
818
|
-
}
|
|
819
|
-
try {
|
|
820
|
-
if (frame !== this.#activeFrame) return;
|
|
821
|
-
|
|
822
|
-
// hack to sync the action event with the frame navigation
|
|
823
|
-
await this.storeScreenshot({
|
|
824
|
-
element: { inputID: "frame" },
|
|
182
|
+
#currentURL = "";
|
|
183
|
+
#activeFrame = null;
|
|
184
|
+
#mode = "noop";
|
|
185
|
+
#previousMode = "noop";
|
|
186
|
+
#remoteDebuggerPort = null;
|
|
187
|
+
envName;
|
|
188
|
+
projectDir;
|
|
189
|
+
TOKEN;
|
|
190
|
+
sendEvent;
|
|
191
|
+
logger;
|
|
192
|
+
screenshotMap = new Map();
|
|
193
|
+
snapshotMap = new Map();
|
|
194
|
+
scenariosStepsMap = new Map();
|
|
195
|
+
namesService;
|
|
196
|
+
workspaceService;
|
|
197
|
+
pageSet = new Set();
|
|
198
|
+
lastKnownUrlPath = "";
|
|
199
|
+
world = { attach: () => { } };
|
|
200
|
+
shouldTakeScreenshot = true;
|
|
201
|
+
watcher = null;
|
|
202
|
+
networkEventsFolder;
|
|
203
|
+
tempProjectFolder;
|
|
204
|
+
tempSnapshotsFolder;
|
|
205
|
+
config = {};
|
|
206
|
+
bvtContext;
|
|
207
|
+
stepRunner;
|
|
208
|
+
context;
|
|
209
|
+
web;
|
|
210
|
+
page;
|
|
211
|
+
browser = null;
|
|
212
|
+
backgroundBrowser;
|
|
213
|
+
backgroundContext;
|
|
214
|
+
timerId = null;
|
|
215
|
+
previousIndex = null;
|
|
216
|
+
previousHistoryLength = null;
|
|
217
|
+
previousEntries = null;
|
|
218
|
+
previousUrl = null;
|
|
219
|
+
scenarioDoc;
|
|
220
|
+
isVerify = false;
|
|
221
|
+
/**
|
|
222
|
+
* @param initialState Initial recorder state and dependencies
|
|
223
|
+
*/
|
|
224
|
+
constructor(initialState) {
|
|
225
|
+
this.envName = initialState.envName;
|
|
226
|
+
this.projectDir = initialState.projectDir;
|
|
227
|
+
this.TOKEN = initialState.TOKEN;
|
|
228
|
+
this.sendEvent = initialState.sendEvent;
|
|
229
|
+
this.logger = initialState.logger;
|
|
230
|
+
this.namesService = new NamesService({
|
|
231
|
+
screenshotMap: this.screenshotMap,
|
|
232
|
+
TOKEN: this.TOKEN,
|
|
233
|
+
projectDir: this.projectDir,
|
|
234
|
+
logger: this.logger,
|
|
825
235
|
});
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
if (changed) {
|
|
833
|
-
this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
|
|
834
|
-
this.#currentURL = newUrl;
|
|
835
|
-
}
|
|
836
|
-
} catch (error) {
|
|
837
|
-
this.logger.error("Error in frame navigate event");
|
|
838
|
-
this.logger.error(error);
|
|
839
|
-
console.error("Error in frame navigate event");
|
|
840
|
-
// console.error(error);
|
|
841
|
-
}
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
hasHistoryReplacementAtIndex(previousEntries, currentEntries, index) {
|
|
846
|
-
if (!previousEntries || !currentEntries) return false;
|
|
847
|
-
if (index >= previousEntries.length || index >= currentEntries.length) return false;
|
|
848
|
-
|
|
849
|
-
const prevEntry = previousEntries[index];
|
|
850
|
-
// console.log("prevEntry", prevEntry);
|
|
851
|
-
const currEntry = currentEntries[index];
|
|
852
|
-
// console.log("currEntry", currEntry);
|
|
853
|
-
|
|
854
|
-
// Check if the entry at this index has been replaced
|
|
855
|
-
return prevEntry.id !== currEntry.id;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// Even simpler approach for your specific case
|
|
859
|
-
analyzeTransitionType(entries, currentIndex, currentEntry) {
|
|
860
|
-
// console.log("Analyzing transition type");
|
|
861
|
-
// console.log("===========================");
|
|
862
|
-
// console.log("Current Index:", currentIndex);
|
|
863
|
-
// console.log("Current Entry:", currentEntry);
|
|
864
|
-
// console.log("Current Entries:", entries);
|
|
865
|
-
// console.log("Current entries length:", entries.length);
|
|
866
|
-
// console.log("===========================");
|
|
867
|
-
// console.log("Previous Index:", this.previousIndex);
|
|
868
|
-
// // console.log("Previous Entry:", this.previousEntries[this.previousIndex]);
|
|
869
|
-
// console.log("Previous Entries:", this.previousEntries);
|
|
870
|
-
// console.log("Previous entries length:", this.previousHistoryLength);
|
|
871
|
-
|
|
872
|
-
if (this.previousIndex === null || this.previousHistoryLength === null || !this.previousEntries) {
|
|
873
|
-
return {
|
|
874
|
-
action: "initial",
|
|
875
|
-
};
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
const indexDiff = currentIndex - this.previousIndex;
|
|
879
|
-
const lengthDiff = entries.length - this.previousHistoryLength;
|
|
880
|
-
|
|
881
|
-
// Backward navigation
|
|
882
|
-
if (indexDiff < 0) {
|
|
883
|
-
return { action: "back" };
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
// Forward navigation
|
|
887
|
-
if (indexDiff > 0 && lengthDiff === 0) {
|
|
888
|
-
// Check if the entry at current index is the same as before
|
|
889
|
-
const entryReplaced = this.hasHistoryReplacementAtIndex(this.previousEntries, entries, currentIndex);
|
|
890
|
-
|
|
891
|
-
if (entryReplaced) {
|
|
892
|
-
return { action: "navigate" }; // New navigation that replaced forward history
|
|
893
|
-
} else {
|
|
894
|
-
return { action: "forward" }; // True forward navigation
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// New navigation (history grew)
|
|
899
|
-
if (lengthDiff > 0) {
|
|
900
|
-
return { action: "navigate" };
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
// Same position, same length
|
|
904
|
-
if (lengthDiff <= 0) {
|
|
905
|
-
const entryReplaced = this.hasHistoryReplacementAtIndex(this.previousEntries, entries, currentIndex);
|
|
906
|
-
|
|
907
|
-
return entryReplaced ? { action: "navigate" } : { action: "reload" };
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
return { action: "unknown" };
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
async getCurrentTransition() {
|
|
914
|
-
if (this?.web?.browser?._name !== "chromium") {
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
const client = await this.context.newCDPSession(this.web.page);
|
|
918
|
-
|
|
919
|
-
try {
|
|
920
|
-
const result = await client.send("Page.getNavigationHistory");
|
|
921
|
-
const entries = result.entries;
|
|
922
|
-
const currentIndex = result.currentIndex;
|
|
923
|
-
|
|
924
|
-
const currentEntry = entries[currentIndex];
|
|
925
|
-
const transitionInfo = this.analyzeTransitionType(entries, currentIndex, currentEntry);
|
|
926
|
-
this.previousIndex = currentIndex;
|
|
927
|
-
this.previousHistoryLength = entries.length;
|
|
928
|
-
this.previousUrl = currentEntry.url;
|
|
929
|
-
this.previousEntries = [...entries]; // Store a copy of current entries
|
|
930
|
-
|
|
931
|
-
return {
|
|
932
|
-
currentEntry,
|
|
933
|
-
navigationAction: transitionInfo.action,
|
|
934
|
-
};
|
|
935
|
-
} catch (error) {
|
|
936
|
-
this.logger.error("Error in getCurrentTransition event");
|
|
937
|
-
this.logger.error(error);
|
|
938
|
-
console.error("Error in getTransistionType event", error);
|
|
939
|
-
} finally {
|
|
940
|
-
await client.detach();
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
userInitiatedTransitionTypes = ["typed", "address_bar"];
|
|
944
|
-
async handlePageTransition() {
|
|
945
|
-
const transition = await this.getCurrentTransition();
|
|
946
|
-
if (!transition) return;
|
|
947
|
-
|
|
948
|
-
const { currentEntry, navigationAction } = transition;
|
|
949
|
-
|
|
950
|
-
switch (navigationAction) {
|
|
951
|
-
case "initial":
|
|
952
|
-
// console.log("Initial navigation, no action taken");
|
|
953
|
-
return;
|
|
954
|
-
case "navigate":
|
|
955
|
-
// console.log("transitionType", transition.transitionType);
|
|
956
|
-
// console.log("sending onGoto event", { url: currentEntry.url,
|
|
957
|
-
// type: "navigate", });
|
|
958
|
-
if (this.userInitiatedTransitionTypes.includes(currentEntry.transitionType)) {
|
|
959
|
-
const env = JSON.parse(readFileSync(this.envName), "utf8");
|
|
960
|
-
const baseUrl = env.baseUrl;
|
|
961
|
-
let url = currentEntry.userTypedURL;
|
|
962
|
-
if (baseUrl && url.startsWith(baseUrl)) {
|
|
963
|
-
url = url.replace(baseUrl, "{{env.baseUrl}}");
|
|
964
|
-
}
|
|
965
|
-
// console.log("User initiated transition");
|
|
966
|
-
this.sendEvent(this.events.onGoto, { url, type: "navigate" });
|
|
236
|
+
this.workspaceService = new PublishService(this.TOKEN);
|
|
237
|
+
this.networkEventsFolder = path.join(tmpdir(), "blinq_network_events");
|
|
238
|
+
this.tempProjectFolder = `${tmpdir()}/bvt_temp_project_${Math.floor(Math.random() * 1000000)}`;
|
|
239
|
+
this.tempSnapshotsFolder = path.join(this.tempProjectFolder, "data/snapshots");
|
|
240
|
+
if (existsSync(this.networkEventsFolder)) {
|
|
241
|
+
rmSync(this.networkEventsFolder, { recursive: true, force: true });
|
|
967
242
|
}
|
|
968
|
-
return;
|
|
969
|
-
case "back":
|
|
970
|
-
// console.log("User navigated back");
|
|
971
|
-
// console.log("sending onGoto event", {
|
|
972
|
-
// type: "back",
|
|
973
|
-
// });
|
|
974
|
-
this.sendEvent(this.events.onGoto, { type: "back" });
|
|
975
|
-
return;
|
|
976
|
-
case "forward":
|
|
977
|
-
// console.log("User navigated forward"); console.log("sending onGoto event", { type: "forward", });
|
|
978
|
-
this.sendEvent(this.events.onGoto, { type: "forward" });
|
|
979
|
-
return;
|
|
980
|
-
default:
|
|
981
|
-
this.sendEvent(this.events.onGoto, { type: "unknown" });
|
|
982
|
-
return;
|
|
983
243
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
this.pageSet.add(page);
|
|
1000
|
-
await page.waitForLoadState("domcontentloaded");
|
|
1001
|
-
|
|
1002
|
-
// add listener for frame navigation on new tab
|
|
1003
|
-
this._addFrameNavigateListener(page);
|
|
1004
|
-
} catch (error) {
|
|
1005
|
-
this.logger.error("Error in page event");
|
|
1006
|
-
this.logger.error(error);
|
|
1007
|
-
console.error("Error in page event");
|
|
1008
|
-
console.error(error);
|
|
1009
|
-
}
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
async openBrowser() {
|
|
1013
|
-
const env = JSON.parse(readFileSync(this.envName), "utf8");
|
|
1014
|
-
const url = env.baseUrl;
|
|
1015
|
-
await this._initBrowser({ url });
|
|
1016
|
-
await this._openTab({ url });
|
|
1017
|
-
process.env.TEMP_RUN = true;
|
|
1018
|
-
}
|
|
1019
|
-
overlayLocators(event) {
|
|
1020
|
-
let locatorsResults = [...event.locators];
|
|
1021
|
-
const cssLocators = event.cssLocators;
|
|
1022
|
-
for (const cssLocator of cssLocators) {
|
|
1023
|
-
locatorsResults.push({ mode: "NO_TEXT", css: cssLocator });
|
|
1024
|
-
}
|
|
1025
|
-
if (event.digitLocators) {
|
|
1026
|
-
for (const digitLocator of event.digitLocators) {
|
|
1027
|
-
digitLocator.mode = "IGNORE_DIGIT";
|
|
1028
|
-
locatorsResults.push(digitLocator);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
if (event.contextLocator) {
|
|
1032
|
-
locatorsResults.push({
|
|
1033
|
-
mode: "CONTEXT",
|
|
1034
|
-
text: event.contextLocator.texts[0],
|
|
1035
|
-
css: event.contextLocator.css,
|
|
1036
|
-
climb: event.contextLocator.climbCount,
|
|
1037
|
-
});
|
|
1038
|
-
}
|
|
1039
|
-
return locatorsResults;
|
|
1040
|
-
}
|
|
1041
|
-
async getScreenShot() {
|
|
1042
|
-
const client = await this.context.newCDPSession(this.web.page);
|
|
1043
|
-
try {
|
|
1044
|
-
// Using CDP to capture the screenshot
|
|
1045
|
-
const { data } = await client.send("Page.captureScreenshot", { format: "png" });
|
|
1046
|
-
return data;
|
|
1047
|
-
} catch (error) {
|
|
1048
|
-
this.logger.error("Error in taking browser screenshot");
|
|
1049
|
-
console.error("Error in taking browser screenshot", error);
|
|
1050
|
-
} finally {
|
|
1051
|
-
await client.detach();
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
async storeScreenshot(event) {
|
|
1055
|
-
try {
|
|
1056
|
-
// const spath = path.join(__dirname, "media", `${event.inputID}.png`);
|
|
1057
|
-
const screenshotURL = await this.getScreenShot();
|
|
1058
|
-
this.screenshotMap.set(event.element.inputID, screenshotURL);
|
|
1059
|
-
// writeFileSync(spath, screenshotURL, "base64");
|
|
1060
|
-
} catch (error) {
|
|
1061
|
-
console.error("Error in saving screenshot: ", error);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
async generateLocators(event) {
|
|
1065
|
-
const snapshotDetails = event.snapshotDetails;
|
|
1066
|
-
if (!snapshotDetails) {
|
|
1067
|
-
throw new Error("No snapshot details found");
|
|
1068
|
-
}
|
|
1069
|
-
const mode = event.mode;
|
|
1070
|
-
const inputID = event.element.inputID;
|
|
1071
|
-
|
|
1072
|
-
const { id, contextId, doc } = snapshotDetails;
|
|
1073
|
-
// const selector = `[data-blinq-id="${id}"]`;
|
|
1074
|
-
const newPage = await this.backgroundContext.newPage();
|
|
1075
|
-
await newPage.setContent(doc, { waitUntil: "domcontentloaded" });
|
|
1076
|
-
const locatorsObj = await newPage.evaluate(
|
|
1077
|
-
([id, contextId, mode]) => {
|
|
1078
|
-
const recorder = window.__bvt_Recorder;
|
|
1079
|
-
const contextElement = document.querySelector(`[data-blinq-context-id="${contextId}"]`);
|
|
1080
|
-
const el = document.querySelector(`[data-blinq-id="${id}"]`);
|
|
1081
|
-
if (contextElement) {
|
|
1082
|
-
const result = recorder.locatorGenerator.toContextLocators(el, contextElement);
|
|
1083
|
-
return result;
|
|
1084
|
-
}
|
|
1085
|
-
const isRecordingText = mode === "recordingText";
|
|
1086
|
-
return recorder.locatorGenerator.getElementLocators(el, {
|
|
1087
|
-
excludeText: isRecordingText,
|
|
1088
|
-
});
|
|
1089
|
-
},
|
|
1090
|
-
[id, contextId, mode]
|
|
1091
|
-
);
|
|
1092
|
-
|
|
1093
|
-
// console.log(`Generated locators: for ${inputID}: `, JSON.stringify(locatorsObj));
|
|
1094
|
-
await newPage.close();
|
|
1095
|
-
if (event.nestFrmLoc?.children) {
|
|
1096
|
-
locatorsObj.nestFrmLoc = event.nestFrmLoc.children;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
this.sendEvent(this.events.updateCommand, {
|
|
1100
|
-
locators: {
|
|
1101
|
-
locators: locatorsObj.locators,
|
|
1102
|
-
nestFrmLoc: locatorsObj.nestFrmLoc,
|
|
1103
|
-
iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
1104
|
-
},
|
|
1105
|
-
allStrategyLocators: locatorsObj.allStrategyLocators,
|
|
1106
|
-
inputID,
|
|
1107
|
-
});
|
|
1108
|
-
// const
|
|
1109
|
-
}
|
|
1110
|
-
async onAction(event) {
|
|
1111
|
-
this._updateUrlPath();
|
|
1112
|
-
// const locators = this.overlayLocators(event);
|
|
1113
|
-
const cmdEvent = {
|
|
1114
|
-
...event.element,
|
|
1115
|
-
...transformAction(
|
|
1116
|
-
event.action,
|
|
1117
|
-
event.element,
|
|
1118
|
-
event.mode === "recordingText" || event.mode === "recordingContext",
|
|
1119
|
-
event.isPopupCloseClick,
|
|
1120
|
-
event.mode === "recordingHover",
|
|
1121
|
-
event.mode === "multiInspecting"
|
|
1122
|
-
),
|
|
1123
|
-
// locators: {
|
|
1124
|
-
// locators: event.locators,
|
|
1125
|
-
// iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
1126
|
-
// },
|
|
1127
|
-
// allStrategyLocators: event.allStrategyLocators,
|
|
1128
|
-
url: event.frame.url,
|
|
1129
|
-
title: event.frame.title,
|
|
1130
|
-
extract: {},
|
|
1131
|
-
lastKnownUrlPath: this.lastKnownUrlPath,
|
|
244
|
+
events = {
|
|
245
|
+
onFrameNavigate: "BVTRecorder.onFrameNavigate",
|
|
246
|
+
onPageClose: "BVTRecorder.onPageClose",
|
|
247
|
+
onBrowserClose: "BVTRecorder.onBrowserClose",
|
|
248
|
+
onNewCommand: "BVTRecorder.command.new",
|
|
249
|
+
onCommandDetails: "BVTRecorder.onCommandDetails",
|
|
250
|
+
onStepDetails: "BVTRecorder.onStepDetails",
|
|
251
|
+
getTestData: "BVTRecorder.getTestData",
|
|
252
|
+
onGoto: "BVTRecorder.onGoto",
|
|
253
|
+
cmdExecutionStart: "BVTRecorder.cmdExecutionStart",
|
|
254
|
+
cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
|
|
255
|
+
cmdExecutionError: "BVTRecorder.cmdExecutionError",
|
|
256
|
+
interceptResults: "BVTRecorder.interceptResults",
|
|
257
|
+
onDebugURLChange: "BVTRecorder.onDebugURLChange",
|
|
258
|
+
updateCommand: "BVTRecorder.updateCommand",
|
|
1132
259
|
};
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
// this._updateUrlPath();
|
|
1142
|
-
if (event.locators) {
|
|
1143
|
-
Object.assign(cmdEvent, {
|
|
1144
|
-
locators: {
|
|
1145
|
-
locators: event.locators,
|
|
1146
|
-
iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
1147
|
-
nestFrmLoc: event.nestFrmLoc?.children,
|
|
260
|
+
bindings = {
|
|
261
|
+
__bvt_recordCommand: async ({ frame, page, context }, event) => {
|
|
262
|
+
this.#activeFrame = frame;
|
|
263
|
+
const nestFrmLoc = await findNestedFrameSelector(frame);
|
|
264
|
+
if (event.statistics?.time !== undefined) {
|
|
265
|
+
this.logger.info(`Time taken for action: ${event.statistics.time}`);
|
|
266
|
+
}
|
|
267
|
+
await this.onAction({ ...event, nestFrmLoc });
|
|
1148
268
|
},
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
1152
|
-
this._updateUrlPath();
|
|
1153
|
-
} else {
|
|
1154
|
-
this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
1155
|
-
this._updateUrlPath();
|
|
1156
|
-
await this.generateLocators(event);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
_updateUrlPath() {
|
|
1160
|
-
try {
|
|
1161
|
-
let url = this.bvtContext.web.page.url();
|
|
1162
|
-
if (url === "about:blank") {
|
|
1163
|
-
return;
|
|
1164
|
-
} else {
|
|
1165
|
-
this.lastKnownUrlPath = new URL(url).pathname;
|
|
1166
|
-
}
|
|
1167
|
-
} catch (error) {
|
|
1168
|
-
console.error("Error in getting last known url path", error);
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
async closeBrowser() {
|
|
1172
|
-
delete process.env.TEMP_RUN;
|
|
1173
|
-
await this.watcher.close().then(() => { });
|
|
1174
|
-
this.watcher = null;
|
|
1175
|
-
this.previousIndex = null;
|
|
1176
|
-
this.previousHistoryLength = null;
|
|
1177
|
-
this.previousUrl = null;
|
|
1178
|
-
this.previousEntries = null;
|
|
1179
|
-
await closeContext();
|
|
1180
|
-
this.pageSet.clear();
|
|
1181
|
-
}
|
|
1182
|
-
async reOpenBrowser(input) {
|
|
1183
|
-
if (input && input.envName) {
|
|
1184
|
-
this.envName = path.join(this.projectDir, "environments", input.envName + ".json");
|
|
1185
|
-
process.env.BLINQ_ENV = this.envName;
|
|
1186
|
-
}
|
|
1187
|
-
await this.closeBrowser();
|
|
1188
|
-
// logger.log("closed");
|
|
1189
|
-
await delay(1000);
|
|
1190
|
-
await this.openBrowser();
|
|
1191
|
-
// logger.log("opened");
|
|
1192
|
-
}
|
|
1193
|
-
async getNumberOfOccurrences({ searchString, regex = false, partial = true, ignoreCase = false, tag = "*" }) {
|
|
1194
|
-
this.isVerify = false;
|
|
1195
|
-
//const script = `window.countStringOccurrences(${JSON.stringify(searchString)});`;
|
|
1196
|
-
if (searchString.length === 0) return -1;
|
|
1197
|
-
let result = 0;
|
|
1198
|
-
for (let i = 0; i < 3; i++) {
|
|
1199
|
-
result = 0;
|
|
1200
|
-
try {
|
|
1201
|
-
// for (const page of this.context.pages()) {
|
|
1202
|
-
const page = this.web.page;
|
|
1203
|
-
for (const frame of page.frames()) {
|
|
1204
|
-
try {
|
|
1205
|
-
//scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params: Params)
|
|
1206
|
-
const frameResult = await this.web._locateElementByText(
|
|
1207
|
-
frame,
|
|
1208
|
-
searchString,
|
|
1209
|
-
tag,
|
|
1210
|
-
regex,
|
|
1211
|
-
partial,
|
|
1212
|
-
ignoreCase,
|
|
1213
|
-
{}
|
|
1214
|
-
);
|
|
1215
|
-
result += frameResult.elementCount;
|
|
1216
|
-
} catch (e) {
|
|
1217
|
-
console.log(e);
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
// }
|
|
1221
|
-
|
|
1222
|
-
return result;
|
|
1223
|
-
} catch (e) {
|
|
1224
|
-
console.log(e);
|
|
1225
|
-
result = 0;
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
async startRecordingInput() {
|
|
1231
|
-
await this.setMode("recordingInput");
|
|
1232
|
-
}
|
|
1233
|
-
async stopRecordingInput() {
|
|
1234
|
-
await this.setMode("idle");
|
|
1235
|
-
}
|
|
1236
|
-
async startRecordingText(isInspectMode) {
|
|
1237
|
-
if (isInspectMode) {
|
|
1238
|
-
await this.setMode("inspecting");
|
|
1239
|
-
} else {
|
|
1240
|
-
await this.setMode("recordingText");
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
async stopRecordingText() {
|
|
1244
|
-
await this.setMode("idle");
|
|
1245
|
-
}
|
|
1246
|
-
async startRecordingContext() {
|
|
1247
|
-
await this.setMode("recordingContext");
|
|
1248
|
-
}
|
|
1249
|
-
async stopRecordingContext() {
|
|
1250
|
-
await this.setMode("idle");
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
async abortExecution() {
|
|
1254
|
-
await this.stepRunner.abortExecution();
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
async pauseExecution({ cmdId }) {
|
|
1258
|
-
await this.stepRunner.pauseExecution(cmdId);
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
async resumeExecution({ cmdId }) {
|
|
1262
|
-
await this.stepRunner.resumeExecution(cmdId);
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
async dealyedRevertMode() {
|
|
1266
|
-
const timerId = setTimeout(async () => {
|
|
1267
|
-
await this.revertMode();
|
|
1268
|
-
}, 100);
|
|
1269
|
-
this.timerId = timerId;
|
|
1270
|
-
}
|
|
1271
|
-
async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork, AICode }, options) {
|
|
1272
|
-
const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
|
|
1273
|
-
|
|
1274
|
-
const env = path.basename(this.envName, ".json");
|
|
1275
|
-
const _env = {
|
|
1276
|
-
TOKEN: this.TOKEN,
|
|
1277
|
-
TEMP_RUN: true,
|
|
1278
|
-
REPORT_FOLDER: this.bvtContext.reportFolder,
|
|
1279
|
-
BLINQ_ENV: this.envName,
|
|
1280
|
-
DEBUG: "blinq:route",
|
|
1281
|
-
// BVT_TEMP_SNAPSHOTS_FOLDER: step.isImplemented ? path.join(this.tempSnapshotsFolder, env) : undefined,
|
|
1282
|
-
};
|
|
1283
|
-
if (!step.isImplemented) {
|
|
1284
|
-
_env.BVT_TEMP_SNAPSHOTS_FOLDER = path.join(this.tempSnapshotsFolder, env);
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
this.bvtContext.navigate = true;
|
|
1288
|
-
this.bvtContext.loadedRoutes = null;
|
|
1289
|
-
if (listenNetwork) {
|
|
1290
|
-
this.bvtContext.STORE_DETAILED_NETWORK_DATA = true;
|
|
1291
|
-
} else {
|
|
1292
|
-
this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
|
|
1293
|
-
}
|
|
1294
|
-
for (const [key, value] of Object.entries(_env)) {
|
|
1295
|
-
process.env[key] = value;
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
if (this.timerId) {
|
|
1299
|
-
clearTimeout(this.timerId);
|
|
1300
|
-
this.timerId = null;
|
|
1301
|
-
}
|
|
1302
|
-
await this.setMode("running");
|
|
1303
|
-
|
|
1304
|
-
try {
|
|
1305
|
-
step.text = step.text.trim();
|
|
1306
|
-
const { result, info } = await this.stepRunner.runStep(
|
|
1307
|
-
{
|
|
1308
|
-
step,
|
|
1309
|
-
parametersMap,
|
|
1310
|
-
envPath: this.envName,
|
|
1311
|
-
tags,
|
|
1312
|
-
config: this.config,
|
|
1313
|
-
AICode,
|
|
269
|
+
__bvt_getMode: async () => {
|
|
270
|
+
return this.#mode;
|
|
1314
271
|
},
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
skipAfter,
|
|
1318
|
-
skipBefore,
|
|
1319
|
-
}
|
|
1320
|
-
);
|
|
1321
|
-
await this.revertMode();
|
|
1322
|
-
return { info };
|
|
1323
|
-
} catch (error) {
|
|
1324
|
-
await this.revertMode();
|
|
1325
|
-
throw error;
|
|
1326
|
-
} finally {
|
|
1327
|
-
for (const key of Object.keys(_env)) {
|
|
1328
|
-
delete process.env[key];
|
|
1329
|
-
}
|
|
1330
|
-
this.bvtContext.navigate = false;
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env, AICode }) {
|
|
1334
|
-
const res = await this.workspaceService.saveScenario({
|
|
1335
|
-
scenario,
|
|
1336
|
-
featureName,
|
|
1337
|
-
override,
|
|
1338
|
-
isSingleStep,
|
|
1339
|
-
branch,
|
|
1340
|
-
isEditing,
|
|
1341
|
-
projectId: path.basename(this.projectDir),
|
|
1342
|
-
env: env ?? this.envName,
|
|
1343
|
-
AICode,
|
|
1344
|
-
});
|
|
1345
|
-
if (res.success) {
|
|
1346
|
-
await this.cleanup({ tags: scenario.tags });
|
|
1347
|
-
} else {
|
|
1348
|
-
throw new Error(res.message || "Error saving scenario");
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
async getImplementedSteps() {
|
|
1352
|
-
const stepsAndScenarios = await getImplementedSteps(this.projectDir);
|
|
1353
|
-
const implementedSteps = stepsAndScenarios.implementedSteps;
|
|
1354
|
-
const scenarios = stepsAndScenarios.scenarios;
|
|
1355
|
-
for (const scenario of scenarios) {
|
|
1356
|
-
this.scenariosStepsMap.set(scenario.name, scenario.steps);
|
|
1357
|
-
delete scenario.steps;
|
|
1358
|
-
}
|
|
1359
|
-
return {
|
|
1360
|
-
implementedSteps,
|
|
1361
|
-
scenarios,
|
|
1362
|
-
};
|
|
1363
|
-
}
|
|
1364
|
-
async getStepsAndCommandsForScenario({ name, featureName }) {
|
|
1365
|
-
const steps = this.scenariosStepsMap.get(name) || [];
|
|
1366
|
-
for (const step of steps) {
|
|
1367
|
-
if (step.isImplemented) {
|
|
1368
|
-
step.commands = this.getCommandsForImplementedStep({ stepName: step.text });
|
|
1369
|
-
} else {
|
|
1370
|
-
step.commands = [];
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
return steps;
|
|
1374
|
-
// return getStepsAndCommandsForScenario({
|
|
1375
|
-
// name,
|
|
1376
|
-
// featureName,
|
|
1377
|
-
// projectDir: this.projectDir,
|
|
1378
|
-
// map: this.scenariosStepsMap,
|
|
1379
|
-
// });
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
async generateStepName({ commands, stepsNames, parameters, map }) {
|
|
1383
|
-
return await this.namesService.generateStepName({ commands, stepsNames, parameters, map });
|
|
1384
|
-
}
|
|
1385
|
-
async generateScenarioAndFeatureNames(scenarioAsText) {
|
|
1386
|
-
return await this.namesService.generateScenarioAndFeatureNames(scenarioAsText);
|
|
1387
|
-
}
|
|
1388
|
-
async generateCommandName({ command }) {
|
|
1389
|
-
return await this.namesService.generateCommandName({ command });
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
async getCurrentChromiumPath() {
|
|
1393
|
-
const currentURL = await this.bvtContext.web.page.url();
|
|
1394
|
-
const env = JSON.parse(readFileSync(this.envName), "utf8");
|
|
1395
|
-
const baseURL = env.baseUrl;
|
|
1396
|
-
const relativeURL = currentURL.startsWith(baseURL) ? currentURL.replace(baseURL, "/") : undefined;
|
|
1397
|
-
return {
|
|
1398
|
-
relativeURL,
|
|
1399
|
-
baseURL,
|
|
1400
|
-
currentURL,
|
|
1401
|
-
};
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
getReportFolder() {
|
|
1405
|
-
if (this.bvtContext.reportFolder) {
|
|
1406
|
-
return this.bvtContext.reportFolder;
|
|
1407
|
-
} else return "";
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
getSnapshotFolder() {
|
|
1411
|
-
if (this.bvtContext.snapshotFolder) {
|
|
1412
|
-
return path.join(process.cwd(), this.bvtContext.snapshotFolder);
|
|
1413
|
-
} else return "";
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
async overwriteTestData(data) {
|
|
1417
|
-
this.bvtContext.stable.overwriteTestData(data.value, this.world);
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
_watchTestData() {
|
|
1421
|
-
this.watcher = chokidar.watch(_getDataFile(this.world, this.bvtContext, this.web), {
|
|
1422
|
-
persistent: true,
|
|
1423
|
-
ignoreInitial: true,
|
|
1424
|
-
awaitWriteFinish: {
|
|
1425
|
-
stabilityThreshold: 2000,
|
|
1426
|
-
pollInterval: 100,
|
|
1427
|
-
},
|
|
1428
|
-
});
|
|
1429
|
-
|
|
1430
|
-
if (existsSync(_getDataFile(this.world, this.bvtContext, this.web))) {
|
|
1431
|
-
try {
|
|
1432
|
-
const testData = JSON.parse(readFileSync(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
|
|
1433
|
-
// this.logger.info("Test data", testData);
|
|
1434
|
-
this.sendEvent(this.events.getTestData, testData);
|
|
1435
|
-
} catch (e) {
|
|
1436
|
-
// this.logger.error("Error reading test data file", e);
|
|
1437
|
-
console.log("Error reading test data file", e);
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
this.logger.info("Watching for test data changes");
|
|
1442
|
-
|
|
1443
|
-
this.watcher.on("all", async (event, path) => {
|
|
1444
|
-
try {
|
|
1445
|
-
const testData = JSON.parse(await readFile(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
|
|
1446
|
-
// this.logger.info("Test data", testData);
|
|
1447
|
-
console.log("Test data changed", testData);
|
|
1448
|
-
this.sendEvent(this.events.getTestData, testData);
|
|
1449
|
-
} catch (e) {
|
|
1450
|
-
// this.logger.error("Error reading test data file", e);
|
|
1451
|
-
console.log("Error reading test data file", e);
|
|
1452
|
-
}
|
|
1453
|
-
});
|
|
1454
|
-
}
|
|
1455
|
-
async loadTestData({ data, type }) {
|
|
1456
|
-
if (type === "user") {
|
|
1457
|
-
const username = data.username;
|
|
1458
|
-
await this.web.loadTestDataAsync("users", username, this.world);
|
|
1459
|
-
} else {
|
|
1460
|
-
const csv = data.csv;
|
|
1461
|
-
const row = data.row;
|
|
1462
|
-
// code = `await context.web.loadTestDataAsync("csv","${csv}:${row}", this)`;
|
|
1463
|
-
await this.web.loadTestDataAsync("csv", `${csv}:${row}`, this.world);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
async discardTestData({ tags }) {
|
|
1468
|
-
resetTestData(this.envName, this.world);
|
|
1469
|
-
await this.cleanup({ tags });
|
|
1470
|
-
}
|
|
1471
|
-
async addToTestData(obj) {
|
|
1472
|
-
if (!existsSync(_getDataFile(this.world, this.bvtContext, this.web))) {
|
|
1473
|
-
await writeFile(_getDataFile(this.world, this.bvtContext, this.web), JSON.stringify({}), "utf8");
|
|
1474
|
-
}
|
|
1475
|
-
let data = JSON.parse(await readFile(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
|
|
1476
|
-
data = Object.assign(data, obj);
|
|
1477
|
-
await writeFile(_getDataFile(this.world, this.bvtContext, this.web), JSON.stringify(data), "utf8");
|
|
1478
|
-
}
|
|
1479
|
-
getScenarios() {
|
|
1480
|
-
const featureFiles = readdirSync(path.join(this.projectDir, "features"))
|
|
1481
|
-
.filter((file) => file.endsWith(".feature"))
|
|
1482
|
-
.map((file) => path.join(this.projectDir, "features", file));
|
|
1483
|
-
try {
|
|
1484
|
-
const parsedFiles = featureFiles.map((file) => this.parseFeatureFile(file));
|
|
1485
|
-
const output = {};
|
|
1486
|
-
parsedFiles.forEach((file) => {
|
|
1487
|
-
if (!file.feature) return;
|
|
1488
|
-
if (!file.feature.name) return;
|
|
1489
|
-
output[file.feature.name] = [];
|
|
1490
|
-
file.feature.children.forEach((child) => {
|
|
1491
|
-
if (child.scenario) {
|
|
1492
|
-
output[file.feature.name].push(child.scenario.name);
|
|
1493
|
-
}
|
|
1494
|
-
});
|
|
1495
|
-
});
|
|
1496
|
-
|
|
1497
|
-
return output;
|
|
1498
|
-
} catch (e) {
|
|
1499
|
-
console.log(e);
|
|
1500
|
-
}
|
|
1501
|
-
return {};
|
|
1502
|
-
}
|
|
1503
|
-
getCommandsForImplementedStep({ stepName }) {
|
|
1504
|
-
const step_definitions = loadStepDefinitions(this.projectDir);
|
|
1505
|
-
const stepParams = parseStepTextParameters(stepName);
|
|
1506
|
-
return getCommandsForImplementedStep(stepName, step_definitions, stepParams).commands;
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
loadExistingScenario({ featureName, scenarioName }) {
|
|
1510
|
-
const step_definitions = loadStepDefinitions(this.projectDir);
|
|
1511
|
-
const featureFilePath = path.join(this.projectDir, "features", featureName);
|
|
1512
|
-
const gherkinDoc = this.parseFeatureFile(featureFilePath);
|
|
1513
|
-
const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
|
|
1514
|
-
this.scenarioDoc = scenario;
|
|
1515
|
-
|
|
1516
|
-
const steps = [];
|
|
1517
|
-
const parameters = [];
|
|
1518
|
-
const datasets = [];
|
|
1519
|
-
if (scenario.examples && scenario.examples.length > 0) {
|
|
1520
|
-
const example = scenario.examples[0];
|
|
1521
|
-
example?.tableHeader?.cells.forEach((cell, index) => {
|
|
1522
|
-
parameters.push({
|
|
1523
|
-
key: cell.value,
|
|
1524
|
-
value: unEscapeNonPrintables(example.tableBody[0].cells[index].value),
|
|
1525
|
-
});
|
|
1526
|
-
// datasets.push({
|
|
1527
|
-
// data: example.tableBody[]
|
|
1528
|
-
// })
|
|
1529
|
-
});
|
|
1530
|
-
|
|
1531
|
-
for (let i = 0; i < example.tableBody.length; i++) {
|
|
1532
|
-
const row = example.tableBody[i];
|
|
1533
|
-
// for (const row of example.tableBody) {
|
|
1534
|
-
const paramters = [];
|
|
1535
|
-
row.cells.forEach((cell, index) => {
|
|
1536
|
-
paramters.push({
|
|
1537
|
-
key: example.tableHeader.cells[index].value,
|
|
1538
|
-
value: unEscapeNonPrintables(cell.value),
|
|
1539
|
-
});
|
|
1540
|
-
});
|
|
1541
|
-
datasets.push({
|
|
1542
|
-
data: paramters,
|
|
1543
|
-
datasetId: i,
|
|
1544
|
-
});
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
for (const step of scenario.steps) {
|
|
1549
|
-
const stepParams = parseStepTextParameters(step.text);
|
|
1550
|
-
// console.log("Parsing step ", step, stepParams);
|
|
1551
|
-
const _s = getCommandsForImplementedStep(step.text, step_definitions, stepParams);
|
|
1552
|
-
delete step.location;
|
|
1553
|
-
const _step = {
|
|
1554
|
-
...step,
|
|
1555
|
-
..._s,
|
|
1556
|
-
keyword: step.keyword.trim(),
|
|
1557
|
-
};
|
|
1558
|
-
parseRouteFiles(this.projectDir, _step);
|
|
1559
|
-
steps.push(_step);
|
|
1560
|
-
}
|
|
1561
|
-
return {
|
|
1562
|
-
name: scenario.name,
|
|
1563
|
-
tags: scenario.tags.map((tag) => tag.name),
|
|
1564
|
-
steps,
|
|
1565
|
-
parameters,
|
|
1566
|
-
datasets,
|
|
1567
|
-
};
|
|
1568
|
-
}
|
|
1569
|
-
async findRelatedTextInAllFrames({ searchString, climb, contextText, params }) {
|
|
1570
|
-
if (searchString.length === 0) return -1;
|
|
1571
|
-
let result = 0;
|
|
1572
|
-
for (let i = 0; i < 3; i++) {
|
|
1573
|
-
result = 0;
|
|
1574
|
-
try {
|
|
1575
|
-
try {
|
|
1576
|
-
const allFrameResult = await this.web.findRelatedTextInAllFrames(
|
|
1577
|
-
contextText,
|
|
1578
|
-
climb,
|
|
1579
|
-
searchString,
|
|
1580
|
-
params,
|
|
1581
|
-
{},
|
|
1582
|
-
this.world
|
|
1583
|
-
);
|
|
1584
|
-
for (const frameResult of allFrameResult) {
|
|
1585
|
-
result += frameResult.elementCount;
|
|
1586
|
-
}
|
|
1587
|
-
} catch (e) {
|
|
1588
|
-
console.log(e);
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
return result;
|
|
1592
|
-
} catch (e) {
|
|
1593
|
-
console.log(e);
|
|
1594
|
-
result = 0;
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
return result;
|
|
1598
|
-
}
|
|
1599
|
-
async setStepCodeByScenario({
|
|
1600
|
-
function_name,
|
|
1601
|
-
mjs_file_content,
|
|
1602
|
-
user_request,
|
|
1603
|
-
selectedTarget,
|
|
1604
|
-
page_context,
|
|
1605
|
-
AIMemory,
|
|
1606
|
-
steps_context,
|
|
1607
|
-
}) {
|
|
1608
|
-
const runsURL = getRunsServiceBaseURL();
|
|
1609
|
-
const url = `${runsURL}/process-user-request/generate-code-with-context`;
|
|
1610
|
-
try {
|
|
1611
|
-
const result = await axiosClient({
|
|
1612
|
-
url,
|
|
1613
|
-
method: "POST",
|
|
1614
|
-
data: {
|
|
1615
|
-
function_name,
|
|
1616
|
-
mjs_file_content,
|
|
1617
|
-
user_request,
|
|
1618
|
-
selectedTarget,
|
|
1619
|
-
page_context,
|
|
1620
|
-
AIMemory,
|
|
1621
|
-
steps_context,
|
|
272
|
+
__bvt_setMode: async (_src, mode) => {
|
|
273
|
+
await this.setMode(mode);
|
|
1622
274
|
},
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
"X-Source": "recorder",
|
|
275
|
+
__bvt_revertMode: async () => {
|
|
276
|
+
await this.revertMode();
|
|
1626
277
|
},
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
return { success: false, message: "Error while fetching code changes" };
|
|
1630
|
-
}
|
|
1631
|
-
return { success: true, data: result.data };
|
|
1632
|
-
} catch (error) {
|
|
1633
|
-
// @ts-ignore
|
|
1634
|
-
const reason = error?.response?.data?.error || "";
|
|
1635
|
-
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1636
|
-
throw new Error(`Failed to fetch code changes: ${errorMessage} \n ${reason}`);
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
async getStepCodeByScenario({ featureName, scenarioName, projectId, branch }) {
|
|
1641
|
-
try {
|
|
1642
|
-
const runsURL = getRunsServiceBaseURL();
|
|
1643
|
-
const ssoURL = runsURL.replace("/runs", "/auth");
|
|
1644
|
-
const privateRepoURL = `${ssoURL}/isRepoPrivate?project_id=${projectId}`;
|
|
1645
|
-
|
|
1646
|
-
const isPrivateRepoReq = await axiosClient({
|
|
1647
|
-
url: privateRepoURL,
|
|
1648
|
-
method: "GET",
|
|
1649
|
-
headers: {
|
|
1650
|
-
Authorization: `Bearer ${this.TOKEN}`,
|
|
1651
|
-
"X-Source": "recorder",
|
|
278
|
+
__bvt_recordPageClose: async ({ page }) => {
|
|
279
|
+
this.pageSet.delete(page);
|
|
1652
280
|
},
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
if (isPrivateRepoReq.status !== 200) {
|
|
1656
|
-
return { success: false, message: "Error while checking repo privacy" };
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
const isPrivateRepo = isPrivateRepoReq.data.isPrivate ? isPrivateRepoReq.data.isPrivate : false;
|
|
1660
|
-
|
|
1661
|
-
const workspaceURL = runsURL.replace("/runs", "/workspace");
|
|
1662
|
-
const url = `${workspaceURL}/get-step-code-by-scenario`;
|
|
1663
|
-
|
|
1664
|
-
const result = await axiosClient({
|
|
1665
|
-
url,
|
|
1666
|
-
method: "POST",
|
|
1667
|
-
data: {
|
|
1668
|
-
scenarioName,
|
|
1669
|
-
featureName,
|
|
1670
|
-
projectId,
|
|
1671
|
-
isPrivateRepo,
|
|
1672
|
-
branch,
|
|
281
|
+
__bvt_closePopups: async () => {
|
|
282
|
+
await this.onClosePopup();
|
|
1673
283
|
},
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
"X-Source": "recorder",
|
|
284
|
+
__bvt_log: async (_src, message) => {
|
|
285
|
+
this.logger.info(`Inside Browser: ${message}`);
|
|
1677
286
|
},
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
return { success: false, message: "Error while getting step code" };
|
|
1681
|
-
}
|
|
1682
|
-
return { success: true, data: result.data.stepInfo };
|
|
1683
|
-
} catch (error) {
|
|
1684
|
-
const reason = error?.response?.data?.error || "";
|
|
1685
|
-
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1686
|
-
throw new Error(`Failed to get step code: ${errorMessage} \n ${reason}`);
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
async getContext() {
|
|
1690
|
-
await this.page.waitForLoadState("domcontentloaded");
|
|
1691
|
-
await this.page.waitForSelector("body");
|
|
1692
|
-
await this.page.waitForTimeout(500);
|
|
1693
|
-
return await this.page.evaluate(() => {
|
|
1694
|
-
return document.documentElement.outerHTML;
|
|
1695
|
-
});
|
|
1696
|
-
}
|
|
1697
|
-
async deleteCommandFromStepCode({ scenario, AICode, command }) {
|
|
1698
|
-
if (!AICode || AICode.length === 0) {
|
|
1699
|
-
console.log("No AI code available to delete.");
|
|
1700
|
-
return;
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
|
|
1704
|
-
const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
|
|
1705
|
-
process.env.tempFeaturesFolderPath = __temp_features_FolderName;
|
|
1706
|
-
process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
|
|
1707
|
-
|
|
1708
|
-
try {
|
|
1709
|
-
await this.stepRunner.copyCodetoTempFolder({ tempFolderPath, AICode });
|
|
1710
|
-
await this.stepRunner.writeWrapperCode(tempFolderPath);
|
|
1711
|
-
const codeView = AICode.find((f) => f.stepName === scenario.step.text);
|
|
1712
|
-
|
|
1713
|
-
if (!codeView) {
|
|
1714
|
-
throw new Error("Step code not found for step: " + scenario.step.text);
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
const functionName = codeView.functionName;
|
|
1718
|
-
const mjsPath = path
|
|
1719
|
-
.normalize(codeView.mjsFile)
|
|
1720
|
-
.split(path.sep)
|
|
1721
|
-
.filter((part) => part !== "features")
|
|
1722
|
-
.join(path.sep);
|
|
1723
|
-
const codePath = path.join(tempFolderPath, mjsPath);
|
|
1724
|
-
|
|
1725
|
-
if (!existsSync(codePath)) {
|
|
1726
|
-
throw new Error("Step code file not found: " + codePath);
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
const codePage = getCodePage(codePath);
|
|
1730
|
-
|
|
1731
|
-
const elements = codePage.getVariableDeclarationAsObject("elements");
|
|
1732
|
-
|
|
1733
|
-
const cucumberStep = getCucumberStep({ step: scenario.step });
|
|
1734
|
-
cucumberStep.text = scenario.step.text;
|
|
1735
|
-
const stepCommands = scenario.step.commands;
|
|
1736
|
-
const cmd = _toRecordingStep(command, scenario.step.name);
|
|
1737
|
-
|
|
1738
|
-
const recording = new Recording();
|
|
1739
|
-
recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
|
|
1740
|
-
const step = { ...recording.steps[0], ...cmd };
|
|
1741
|
-
const result = _generateCodeFromCommand(step, elements, {});
|
|
1742
|
-
|
|
1743
|
-
codePage._removeCommands(functionName, result.codeLines);
|
|
1744
|
-
codePage.removeUnusedElements();
|
|
1745
|
-
codePage.save();
|
|
1746
|
-
|
|
1747
|
-
await rm(tempFolderPath, { recursive: true, force: true });
|
|
1748
|
-
|
|
1749
|
-
return { code: codePage.fileContent, mjsFile: codeView.mjsFile };
|
|
1750
|
-
} catch (error) {
|
|
1751
|
-
await rm(tempFolderPath, { recursive: true, force: true });
|
|
1752
|
-
throw error;
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
async addCommandToStepCode({ scenario, AICode }) {
|
|
1756
|
-
if (!AICode || AICode.length === 0) {
|
|
1757
|
-
console.log("No AI code available to add.");
|
|
1758
|
-
return;
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
|
|
1762
|
-
const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
|
|
1763
|
-
process.env.tempFeaturesFolderPath = __temp_features_FolderName;
|
|
1764
|
-
process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
|
|
1765
|
-
|
|
1766
|
-
try {
|
|
1767
|
-
await this.stepRunner.copyCodetoTempFolder({ tempFolderPath, AICode });
|
|
1768
|
-
await this.stepRunner.writeWrapperCode(tempFolderPath);
|
|
1769
|
-
|
|
1770
|
-
let codeView = AICode.find((f) => f.stepName === scenario.step.text);
|
|
1771
|
-
|
|
1772
|
-
if (codeView) {
|
|
1773
|
-
scenario.step.commands = [scenario.step.commands.pop()];
|
|
1774
|
-
const functionName = codeView.functionName;
|
|
1775
|
-
const mjsPath = path
|
|
1776
|
-
.normalize(codeView.mjsFile)
|
|
1777
|
-
.split(path.sep)
|
|
1778
|
-
.filter((part) => part !== "features")
|
|
1779
|
-
.join(path.sep);
|
|
1780
|
-
const codePath = path.join(tempFolderPath, mjsPath);
|
|
1781
|
-
|
|
1782
|
-
if (!existsSync(codePath)) {
|
|
1783
|
-
throw new Error("Step code file not found: " + codePath);
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
const codePage = getCodePage(codePath);
|
|
1787
|
-
const elements = codePage.getVariableDeclarationAsObject("elements");
|
|
1788
|
-
|
|
1789
|
-
const cucumberStep = getCucumberStep({ step: scenario.step });
|
|
1790
|
-
cucumberStep.text = scenario.step.text;
|
|
1791
|
-
const stepCommands = scenario.step.commands;
|
|
1792
|
-
const cmd = _toRecordingStep(scenario.step.commands[0], scenario.step.name);
|
|
1793
|
-
|
|
1794
|
-
const recording = new Recording();
|
|
1795
|
-
recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
|
|
1796
|
-
const step = { ...recording.steps[0], ...cmd };
|
|
1797
|
-
|
|
1798
|
-
const result = _generateCodeFromCommand(step, elements, {});
|
|
1799
|
-
codePage.insertElements(result.elements);
|
|
1800
|
-
|
|
1801
|
-
codePage._injectOneCommand(functionName, result.codeLines.join("\n"));
|
|
1802
|
-
codePage.save();
|
|
1803
|
-
|
|
1804
|
-
await rm(tempFolderPath, { recursive: true, force: true });
|
|
1805
|
-
|
|
1806
|
-
return { code: codePage.fileContent, newStep: false, mjsFile: codeView.mjsFile };
|
|
1807
|
-
}
|
|
1808
|
-
console.log("Step code not found for step: ", scenario.step.text);
|
|
1809
|
-
|
|
1810
|
-
codeView = AICode[0];
|
|
1811
|
-
const functionName = toMethodName(scenario.step.text);
|
|
1812
|
-
const codeLines = [];
|
|
1813
|
-
const mjsPath = path
|
|
1814
|
-
.normalize(codeView.mjsFile)
|
|
1815
|
-
.split(path.sep)
|
|
1816
|
-
.filter((part) => part !== "features")
|
|
1817
|
-
.join(path.sep);
|
|
1818
|
-
const codePath = path.join(tempFolderPath, mjsPath);
|
|
1819
|
-
|
|
1820
|
-
if (!existsSync(codePath)) {
|
|
1821
|
-
throw new Error("Step code file not found: " + codePath);
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
const codePage = getCodePage(codePath);
|
|
1825
|
-
const elements = codePage.getVariableDeclarationAsObject("elements");
|
|
1826
|
-
let newElements = { ...elements };
|
|
1827
|
-
|
|
1828
|
-
const cucumberStep = getCucumberStep({ step: scenario.step });
|
|
1829
|
-
cucumberStep.text = scenario.step.text;
|
|
1830
|
-
const stepCommands = scenario.step.commands;
|
|
1831
|
-
stepCommands.forEach((command) => {
|
|
1832
|
-
const cmd = _toRecordingStep(command, scenario.step.name);
|
|
1833
|
-
|
|
1834
|
-
const recording = new Recording();
|
|
1835
|
-
recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
|
|
1836
|
-
const step = { ...recording.steps[0], ...cmd };
|
|
1837
|
-
const result = _generateCodeFromCommand(step, elements, {});
|
|
1838
|
-
newElements = { ...result.elements };
|
|
1839
|
-
codeLines.push(...result.codeLines);
|
|
1840
|
-
});
|
|
1841
|
-
|
|
1842
|
-
codePage.insertElements(newElements);
|
|
1843
|
-
codePage.addInfraCommand(
|
|
1844
|
-
functionName,
|
|
1845
|
-
cucumberStep.text,
|
|
1846
|
-
cucumberStep.getVariablesList(),
|
|
1847
|
-
codeLines,
|
|
1848
|
-
false,
|
|
1849
|
-
"recorder"
|
|
1850
|
-
);
|
|
1851
|
-
|
|
1852
|
-
const keyword = (cucumberStep.keywordAlias ?? cucumberStep.keyword).trim();
|
|
1853
|
-
codePage.addCucumberStep(keyword, cucumberStep.getTemplate(), functionName, stepCommands.length);
|
|
1854
|
-
codePage.save();
|
|
1855
|
-
|
|
1856
|
-
await rm(tempFolderPath, { recursive: true, force: true });
|
|
1857
|
-
|
|
1858
|
-
return { code: codePage.fileContent, newStep: true, functionName, mjsFile: codeView.mjsFile };
|
|
1859
|
-
} catch (error) {
|
|
1860
|
-
await rm(tempFolderPath, { recursive: true, force: true });
|
|
1861
|
-
throw error;
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
async cleanup({ tags }) {
|
|
1865
|
-
const noopStep = {
|
|
1866
|
-
text: "Noop",
|
|
1867
|
-
isImplemented: true,
|
|
1868
|
-
};
|
|
1869
|
-
const projectDir = this.projectDir;
|
|
1870
|
-
console.log("Cleaning up project dir:", projectDir);
|
|
1871
|
-
|
|
1872
|
-
try {
|
|
1873
|
-
// run a dummy scenario that will run after hooks
|
|
1874
|
-
await this.runStep(
|
|
1875
|
-
{
|
|
1876
|
-
step: noopStep,
|
|
1877
|
-
parametersMap: {},
|
|
1878
|
-
tags: tags || [],
|
|
287
|
+
__bvt_getObject: (_src, obj) => {
|
|
288
|
+
this.processObject(obj);
|
|
1879
289
|
},
|
|
1880
|
-
{
|
|
1881
|
-
skipAfter: false,
|
|
1882
|
-
}
|
|
1883
|
-
);
|
|
1884
|
-
|
|
1885
|
-
// delete the temp folders (any folder that starts with __temp_features)
|
|
1886
|
-
const tempFolders = readdirSync(projectDir).filter((folder) => folder.startsWith("__temp_features"));
|
|
1887
|
-
for (const folder of tempFolders) {
|
|
1888
|
-
const folderPath = path.join(projectDir, folder);
|
|
1889
|
-
if (existsSync(folderPath)) {
|
|
1890
|
-
this.logger.info(`Deleting temp folder: ${folderPath}`);
|
|
1891
|
-
rmSync(folderPath, { recursive: true });
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
} catch (error) {
|
|
1895
|
-
console.error("Error in cleanup", error);
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
async processAriaSnapshot(snapshot) {
|
|
1899
|
-
try {
|
|
1900
|
-
await this.evaluateInAllFrames(
|
|
1901
|
-
this.context,
|
|
1902
|
-
`window.__bvt_Recorder.processAriaSnapshot(${JSON.stringify(snapshot)});`
|
|
1903
|
-
);
|
|
1904
|
-
return true;
|
|
1905
|
-
} catch (e) {
|
|
1906
|
-
return false;
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
async deselectAriaElements() {
|
|
1910
|
-
try {
|
|
1911
|
-
await this.evaluateInAllFrames(this.context, `window.__bvt_Recorder.deselectAriaElements();`);
|
|
1912
|
-
return true;
|
|
1913
|
-
} catch (e) {
|
|
1914
|
-
return false;
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
async initExecution({ tags = [] }) {
|
|
1918
|
-
// run before hooks
|
|
1919
|
-
const noopStep = {
|
|
1920
|
-
text: "Noop",
|
|
1921
|
-
isImplemented: true,
|
|
1922
290
|
};
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
},
|
|
1929
|
-
{
|
|
1930
|
-
skipBefore: false,
|
|
1931
|
-
skipAfter: true,
|
|
1932
|
-
}
|
|
1933
|
-
);
|
|
1934
|
-
}
|
|
1935
|
-
async cleanupExecution({ tags = [] }) {
|
|
1936
|
-
// run after hooks
|
|
1937
|
-
const noopStep = {
|
|
1938
|
-
text: "Noop",
|
|
1939
|
-
isImplemented: true,
|
|
291
|
+
getSnapshot = async (attr) => {
|
|
292
|
+
const selector = `[__bvt_snapshot="${attr}"]`;
|
|
293
|
+
const locator = await this.web.page.locator(selector);
|
|
294
|
+
const snapshot = await locator.ariaSnapshot();
|
|
295
|
+
return snapshot;
|
|
1940
296
|
};
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
Object.keys(params).forEach((key) => {
|
|
1985
|
-
if (!params[key].startsWith("{{") || !params[key].endsWith("}}")) {
|
|
1986
|
-
newFakeParams[key] = params[key];
|
|
1987
|
-
return;
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
try {
|
|
1991
|
-
const value = params[key].substring(2, params[key].length - 2).trim();
|
|
1992
|
-
const faking = value.split("(")[0].split(".");
|
|
1993
|
-
let argument = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
|
|
1994
|
-
argument = isNaN(Number(argument)) || argument === "" ? argument : Number(argument);
|
|
1995
|
-
let fakeFunc = faker;
|
|
1996
|
-
faking.forEach((f) => {
|
|
1997
|
-
fakeFunc = fakeFunc[f];
|
|
297
|
+
processObject = async ({ type, action, value }) => {
|
|
298
|
+
switch (type) {
|
|
299
|
+
case "snapshot-element": {
|
|
300
|
+
if (action === "get-template") {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
default: {
|
|
306
|
+
console.log("Unknown object type", type);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
getPWScript() {
|
|
312
|
+
const pwFolder = path.join(__dirname, "..", "..", "assets", "preload", "pw_utils");
|
|
313
|
+
const result = [];
|
|
314
|
+
for (const script of readdirSync(pwFolder)) {
|
|
315
|
+
const scriptPath = path.join(pwFolder, script);
|
|
316
|
+
const content = readFileSync(scriptPath, "utf8");
|
|
317
|
+
result.push(content);
|
|
318
|
+
}
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
getRecorderScripts() {
|
|
322
|
+
const recorderFolder = path.join(__dirname, "..", "..", "assets", "preload", "recorder");
|
|
323
|
+
const result = [];
|
|
324
|
+
for (const script of readdirSync(recorderFolder)) {
|
|
325
|
+
const scriptPath = path.join(recorderFolder, script);
|
|
326
|
+
const content = readFileSync(scriptPath, "utf8");
|
|
327
|
+
result.push(content);
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
getInitScripts(config) {
|
|
332
|
+
return getInitScript(config, {
|
|
333
|
+
sdkLanguage: "javascript",
|
|
334
|
+
testIdAttributeName: "blinq-test-id",
|
|
335
|
+
stableRafCount: 0,
|
|
336
|
+
browserName: this.browser?.browserType().name(),
|
|
337
|
+
inputFileRoleTextbox: false,
|
|
338
|
+
customEngines: [],
|
|
339
|
+
isUnderTest: true,
|
|
1998
340
|
});
|
|
1999
|
-
const newValue = fakeFunc(argument);
|
|
2000
|
-
newFakeParams[key] = newValue;
|
|
2001
|
-
} catch (error) {
|
|
2002
|
-
newFakeParams[key] = params[key];
|
|
2003
|
-
}
|
|
2004
|
-
});
|
|
2005
|
-
|
|
2006
|
-
return newFakeParams;
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
async getBrowserState() {
|
|
2010
|
-
try {
|
|
2011
|
-
const state = await this.browserEmitter?.getState();
|
|
2012
|
-
this.sendEvent(this.events.browserStateSync, state);
|
|
2013
|
-
} catch (error) {
|
|
2014
|
-
this.logger.error("Error getting browser state:", error);
|
|
2015
|
-
this.sendEvent(this.events.browserStateError, {
|
|
2016
|
-
message: "Error getting browser state",
|
|
2017
|
-
code: "GET_STATE_ERROR",
|
|
2018
|
-
});
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
async applyClipboardPayload(message) {
|
|
2023
|
-
const payload = message?.data ?? message;
|
|
2024
|
-
|
|
2025
|
-
this.logger.info("[BVTRecorder] applyClipboardPayload called", {
|
|
2026
|
-
hasPayload: !!payload,
|
|
2027
|
-
hasText: !!payload?.text,
|
|
2028
|
-
hasHtml: !!payload?.html,
|
|
2029
|
-
trigger: message?.trigger,
|
|
2030
|
-
});
|
|
2031
|
-
|
|
2032
|
-
if (!payload) {
|
|
2033
|
-
this.logger.warn("[BVTRecorder] No payload provided");
|
|
2034
|
-
return;
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
try {
|
|
2038
|
-
if (this.browserEmitter && typeof this.browserEmitter.applyClipboardPayload === "function") {
|
|
2039
|
-
this.logger.info("[BVTRecorder] Using RemoteBrowserService to apply clipboard");
|
|
2040
|
-
await this.browserEmitter.applyClipboardPayload(payload);
|
|
2041
|
-
return;
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
2045
|
-
if (!activePage) {
|
|
2046
|
-
this.logger.warn("[BVTRecorder] No active page available");
|
|
2047
|
-
return;
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
this.logger.info("[BVTRecorder] Applying clipboard to page", {
|
|
2051
|
-
url: activePage.url(),
|
|
2052
|
-
isClosed: activePage.isClosed(),
|
|
2053
|
-
});
|
|
2054
|
-
|
|
2055
|
-
const result = await activePage.evaluate((clipboardData) => {
|
|
2056
|
-
console.log("[Page] Executing clipboard application", clipboardData);
|
|
2057
|
-
if (typeof window.__bvt_applyClipboardData === "function") {
|
|
2058
|
-
return window.__bvt_applyClipboardData(clipboardData);
|
|
2059
|
-
}
|
|
2060
|
-
console.error("[Page] __bvt_applyClipboardData function not found!");
|
|
2061
|
-
return false;
|
|
2062
|
-
}, payload);
|
|
2063
|
-
|
|
2064
|
-
this.logger.info("[BVTRecorder] Clipboard application result:", result);
|
|
2065
|
-
|
|
2066
|
-
if (!result) {
|
|
2067
|
-
this.logger.warn("[BVTRecorder] Clipboard data not applied successfully");
|
|
2068
|
-
} else {
|
|
2069
|
-
this.logger.info("[BVTRecorder] Clipboard data applied successfully");
|
|
2070
|
-
}
|
|
2071
|
-
} catch (error) {
|
|
2072
|
-
this.logger.error("[BVTRecorder] Error applying clipboard payload", error);
|
|
2073
|
-
this.sendEvent(this.events.clipboardError, {
|
|
2074
|
-
message: "Failed to apply clipboard contents to the remote session",
|
|
2075
|
-
trigger: message?.trigger ?? "paste",
|
|
2076
|
-
});
|
|
2077
|
-
}
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
|
-
hasClipboardPayload(payload) {
|
|
2081
|
-
return Boolean(
|
|
2082
|
-
payload && (payload.text || payload.html || (Array.isArray(payload.files) && payload.files.length > 0))
|
|
2083
|
-
);
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
async collectClipboardFromPage(page) {
|
|
2087
|
-
if (!page) {
|
|
2088
|
-
this.logger.warn("[BVTRecorder] No page available to collect clipboard data");
|
|
2089
|
-
return null;
|
|
2090
341
|
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
342
|
+
async _initBrowser({ url }) {
|
|
343
|
+
if (process.env.CDP_LISTEN_PORT === undefined) {
|
|
344
|
+
this.#remoteDebuggerPort = await findAvailablePort();
|
|
345
|
+
process.env.CDP_LISTEN_PORT = String(this.#remoteDebuggerPort);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
this.#remoteDebuggerPort = Number(process.env.CDP_LISTEN_PORT);
|
|
349
|
+
}
|
|
350
|
+
// this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
|
|
351
|
+
this.world = { attach: () => { } };
|
|
352
|
+
const ai_config_file = path.join(this.projectDir, "ai_config.json");
|
|
353
|
+
let ai_config = {};
|
|
354
|
+
if (existsSync(ai_config_file)) {
|
|
355
|
+
try {
|
|
356
|
+
ai_config = JSON.parse(readFileSync(ai_config_file, "utf8"));
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
this.logger.error("Error reading ai_config.json", { error });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
this.config = ai_config;
|
|
363
|
+
const initScripts = {
|
|
364
|
+
recorderCjs: null,
|
|
365
|
+
scripts: [
|
|
366
|
+
this.getInitScripts(ai_config),
|
|
367
|
+
`\ndelete Object.getPrototypeOf(navigator).webdriver;${process.env.WINDOW_DEBUGGER ? "window.debug=true;\n" : ""}`,
|
|
368
|
+
],
|
|
369
|
+
};
|
|
370
|
+
const bvtContext = (await initContext(url, false, false, this.world, 450, initScripts, this.envName));
|
|
371
|
+
this.bvtContext = bvtContext;
|
|
372
|
+
this.stepRunner = new BVTStepRunner({
|
|
373
|
+
projectDir: this.projectDir,
|
|
374
|
+
sendExecutionStatus: (data) => {
|
|
375
|
+
if (data && data.type) {
|
|
376
|
+
switch (data.type) {
|
|
377
|
+
case "cmdExecutionStart":
|
|
378
|
+
this.sendEvent(this.events.cmdExecutionStart, data);
|
|
379
|
+
break;
|
|
380
|
+
case "cmdExecutionSuccess":
|
|
381
|
+
this.sendEvent(this.events.cmdExecutionSuccess, data);
|
|
382
|
+
break;
|
|
383
|
+
case "cmdExecutionError":
|
|
384
|
+
this.sendEvent(this.events.cmdExecutionError, data);
|
|
385
|
+
break;
|
|
386
|
+
case "interceptResults":
|
|
387
|
+
this.sendEvent(this.events.interceptResults, data);
|
|
388
|
+
break;
|
|
389
|
+
default:
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
bvtContext: this.bvtContext,
|
|
2097
395
|
});
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
396
|
+
this.context = bvtContext.playContext;
|
|
397
|
+
this.web = bvtContext.stable || bvtContext.web;
|
|
398
|
+
this.web.tryAllStrategies = true;
|
|
399
|
+
this.page = bvtContext.page;
|
|
400
|
+
this.pageSet.add(this.page);
|
|
401
|
+
this.lastKnownUrlPath = this._updateUrlPath();
|
|
402
|
+
const browser = await this.context.browser();
|
|
403
|
+
this.browser = browser;
|
|
404
|
+
// add bindings
|
|
405
|
+
for (const [name, handler] of Object.entries(this.bindings)) {
|
|
406
|
+
await this.context.exposeBinding(name, handler);
|
|
407
|
+
}
|
|
408
|
+
this._watchTestData();
|
|
409
|
+
this.web.onRestoreSaveState = async (url) => {
|
|
410
|
+
await this._initBrowser({ url });
|
|
411
|
+
this._addPagelisteners(this.context);
|
|
412
|
+
this._addFrameNavigateListener(this.page);
|
|
2114
413
|
};
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
414
|
+
// create a second browser for locator generation
|
|
415
|
+
this.backgroundBrowser = await chromium.launch({
|
|
416
|
+
headless: true,
|
|
417
|
+
});
|
|
418
|
+
this.backgroundContext = await this.backgroundBrowser.newContext({});
|
|
419
|
+
await this.backgroundContext.addInitScript({ content: this.getInitScripts(this.config) });
|
|
420
|
+
await this.backgroundContext.newPage();
|
|
421
|
+
}
|
|
422
|
+
async onClosePopup() {
|
|
423
|
+
// console.log("close popups");
|
|
424
|
+
await this.bvtContext.web.closeUnexpectedPopups();
|
|
425
|
+
}
|
|
426
|
+
async evaluateInAllFrames(context, script) {
|
|
427
|
+
// retry 3 times
|
|
428
|
+
for (let i = 0; i < 3; i++) {
|
|
429
|
+
try {
|
|
430
|
+
for (const page of context.pages()) {
|
|
431
|
+
await evaluate(page.mainFrame(), script);
|
|
2133
432
|
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
// console.error("Error evaluting in context:", error);
|
|
437
|
+
this.logger.error("Error evaluating in context", { error });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
getMode() {
|
|
442
|
+
// console.log("getMode", this.#mode);
|
|
443
|
+
this.logger.info("Current mode", { mode: this.#mode });
|
|
444
|
+
return this.#mode;
|
|
445
|
+
}
|
|
446
|
+
async setMode(mode) {
|
|
447
|
+
await this.evaluateInAllFrames(this.context, `window.__bvt_Recorder.mode = "${mode}";`);
|
|
448
|
+
this.#previousMode = this.#mode;
|
|
449
|
+
this.#mode = mode;
|
|
450
|
+
}
|
|
451
|
+
async revertMode() {
|
|
452
|
+
await this.setMode(this.#previousMode);
|
|
453
|
+
}
|
|
454
|
+
async _openTab({ url }) {
|
|
455
|
+
// add listeners for new pages
|
|
456
|
+
this._addPagelisteners(this.context);
|
|
457
|
+
await this.page.goto(url, {
|
|
458
|
+
waitUntil: "domcontentloaded",
|
|
459
|
+
timeout: typeof this.config.page_timeout === "number" ? this.config.page_timeout : 60_000,
|
|
460
|
+
});
|
|
461
|
+
// add listener for frame navigation on current tab
|
|
462
|
+
this._addFrameNavigateListener(this.page);
|
|
463
|
+
// eval init script on current tab
|
|
464
|
+
// await this._initPage(this.page);
|
|
465
|
+
this.#currentURL = url;
|
|
466
|
+
await this.page.dispatchEvent("html", "scroll");
|
|
467
|
+
await delay(1000);
|
|
468
|
+
}
|
|
469
|
+
_addFrameNavigateListener(page) {
|
|
470
|
+
page.on("close", () => {
|
|
471
|
+
try {
|
|
472
|
+
if (!this.pageSet.has(page))
|
|
473
|
+
return;
|
|
474
|
+
// console.log(this.context.pages().length);
|
|
475
|
+
if (this.context.pages().length > 0) {
|
|
476
|
+
this.sendEvent(this.events.onPageClose);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
// closed all tabs
|
|
480
|
+
this.sendEvent(this.events.onBrowserClose);
|
|
2145
481
|
}
|
|
2146
|
-
}
|
|
2147
482
|
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
if (!result.text && typeof navigator.clipboard.readText === "function") {
|
|
2154
|
-
try {
|
|
2155
|
-
const text = await navigator.clipboard.readText();
|
|
2156
|
-
if (text) {
|
|
2157
|
-
result.text = text;
|
|
483
|
+
catch (error) {
|
|
484
|
+
this.logger.error("Error in page close event", { error });
|
|
485
|
+
console.error("Error in page close event");
|
|
486
|
+
console.error(error);
|
|
2158
487
|
}
|
|
2159
|
-
} catch (error) {
|
|
2160
|
-
console.warn("[BVTRecorder] navigator.clipboard.readText failed", error);
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
if (!result.text) {
|
|
2165
|
-
const selection = window.getSelection?.()?.toString?.();
|
|
2166
|
-
if (selection) {
|
|
2167
|
-
result.text = selection;
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
if (files.length > 0) {
|
|
2172
|
-
result.files = files;
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
return result;
|
|
2176
|
-
});
|
|
2177
|
-
|
|
2178
|
-
return payload;
|
|
2179
|
-
} catch (error) {
|
|
2180
|
-
this.logger.error("[BVTRecorder] Error collecting clipboard payload", error);
|
|
2181
|
-
return null;
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
async readClipboardPayload(message) {
|
|
2186
|
-
try {
|
|
2187
|
-
let payload = null;
|
|
2188
|
-
if (this.browserEmitter && typeof this.browserEmitter.readClipboardPayload === "function") {
|
|
2189
|
-
payload = await this.browserEmitter.readClipboardPayload();
|
|
2190
|
-
} else {
|
|
2191
|
-
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
2192
|
-
payload = await this.collectClipboardFromPage(activePage);
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
if (this.hasClipboardPayload(payload)) {
|
|
2196
|
-
this.logger.info("[BVTRecorder] Remote clipboard payload ready", {
|
|
2197
|
-
hasText: !!payload.text,
|
|
2198
|
-
hasHtml: !!payload.html,
|
|
2199
|
-
files: payload.files?.length ?? 0,
|
|
2200
488
|
});
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
489
|
+
page.on("framenavigated", async (frame) => {
|
|
490
|
+
try {
|
|
491
|
+
if (frame !== page.mainFrame())
|
|
492
|
+
return;
|
|
493
|
+
await this.handlePageTransition();
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
this.logger.error("Error in handlePageTransition event", { error });
|
|
497
|
+
console.error("Error in handlePageTransition event");
|
|
498
|
+
console.error(error);
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
if (frame !== this.#activeFrame)
|
|
502
|
+
return;
|
|
503
|
+
// hack to sync the action event with the frame navigation
|
|
504
|
+
await this.storeScreenshot({
|
|
505
|
+
element: { inputID: "frame" },
|
|
506
|
+
});
|
|
507
|
+
const newUrl = frame.url();
|
|
508
|
+
const newPath = new URL(newUrl).pathname;
|
|
509
|
+
const newTitle = await frame.title();
|
|
510
|
+
const changed = diffPaths(this.#currentURL, newUrl);
|
|
511
|
+
if (changed) {
|
|
512
|
+
this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
|
|
513
|
+
this.#currentURL = newUrl;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
this.logger.error("Error in frame navigate event", { error });
|
|
518
|
+
console.error("Error in frame navigate event");
|
|
519
|
+
// console.error(error);
|
|
520
|
+
}
|
|
2205
521
|
});
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
const
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
522
|
+
}
|
|
523
|
+
hasHistoryReplacementAtIndex(previousEntries, currentEntries, index) {
|
|
524
|
+
if (!previousEntries || !currentEntries)
|
|
525
|
+
return false;
|
|
526
|
+
if (index >= previousEntries.length || index >= currentEntries.length)
|
|
527
|
+
return false;
|
|
528
|
+
const prevEntry = previousEntries[index];
|
|
529
|
+
// console.log("prevEntry", prevEntry);
|
|
530
|
+
const currEntry = currentEntries[index];
|
|
531
|
+
// console.log("currEntry", currEntry);
|
|
532
|
+
// Check if the entry at this index has been replaced
|
|
533
|
+
return prevEntry.id !== currEntry.id;
|
|
534
|
+
}
|
|
535
|
+
// Even simpler approach for your specific case
|
|
536
|
+
analyzeTransitionType(entries, currentIndex, currentEntry) {
|
|
537
|
+
// console.log("Analyzing transition type");
|
|
538
|
+
// console.log("===========================");
|
|
539
|
+
// console.log("Current Index:", currentIndex);
|
|
540
|
+
// console.log("Current Entry:", currentEntry);
|
|
541
|
+
// console.log("Current Entries:", entries);
|
|
542
|
+
// console.log("Current entries length:", entries.length);
|
|
543
|
+
// console.log("===========================");
|
|
544
|
+
// console.log("Previous Index:", this.previousIndex);
|
|
545
|
+
// // console.log("Previous Entry:", this.previousEntries[this.previousIndex]);
|
|
546
|
+
// console.log("Previous Entries:", this.previousEntries);
|
|
547
|
+
// console.log("Previous entries length:", this.previousHistoryLength);
|
|
548
|
+
if (this.previousIndex === null || this.previousHistoryLength === null || !this.previousEntries) {
|
|
549
|
+
return {
|
|
550
|
+
action: "initial",
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
const indexDiff = currentIndex - this.previousIndex;
|
|
554
|
+
const lengthDiff = entries.length - this.previousHistoryLength;
|
|
555
|
+
// Backward navigation
|
|
556
|
+
if (indexDiff < 0) {
|
|
557
|
+
return { action: "back" };
|
|
558
|
+
}
|
|
559
|
+
// Forward navigation
|
|
560
|
+
if (indexDiff > 0 && lengthDiff === 0) {
|
|
561
|
+
// Check if the entry at current index is the same as before
|
|
562
|
+
const entryReplaced = this.hasHistoryReplacementAtIndex(this.previousEntries, entries, currentIndex);
|
|
563
|
+
if (entryReplaced) {
|
|
564
|
+
return { action: "navigate" }; // New navigation that replaced forward history
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
return { action: "forward" }; // True forward navigation
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// New navigation (history grew)
|
|
571
|
+
if (lengthDiff > 0) {
|
|
572
|
+
return { action: "navigate" };
|
|
573
|
+
}
|
|
574
|
+
// Same position, same length
|
|
575
|
+
if (lengthDiff <= 0) {
|
|
576
|
+
const entryReplaced = this.hasHistoryReplacementAtIndex(this.previousEntries, entries, currentIndex);
|
|
577
|
+
return entryReplaced ? { action: "navigate" } : { action: "reload" };
|
|
578
|
+
}
|
|
579
|
+
return { action: "unknown" };
|
|
580
|
+
}
|
|
581
|
+
async getCurrentTransition() {
|
|
582
|
+
if (this?.web?.browser?._name !== "chromium") {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const client = await this.context.newCDPSession(this.web.page);
|
|
586
|
+
try {
|
|
587
|
+
const result = await client.send("Page.getNavigationHistory");
|
|
588
|
+
const entries = result.entries;
|
|
589
|
+
const currentIndex = result.currentIndex;
|
|
590
|
+
const currentEntry = entries[currentIndex];
|
|
591
|
+
const transitionInfo = this.analyzeTransitionType(entries, currentIndex, currentEntry);
|
|
592
|
+
this.previousIndex = currentIndex;
|
|
593
|
+
this.previousHistoryLength = entries.length;
|
|
594
|
+
this.previousUrl = currentEntry.url;
|
|
595
|
+
this.previousEntries = [...entries]; // Store a copy of current entries
|
|
596
|
+
return {
|
|
597
|
+
currentEntry,
|
|
598
|
+
navigationAction: transitionInfo.action,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
this.logger.error("Error in getCurrentTransition event", { error });
|
|
603
|
+
console.error("Error in getTransistionType event", error);
|
|
604
|
+
}
|
|
605
|
+
finally {
|
|
606
|
+
await client.detach();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
userInitiatedTransitionTypes = ["typed", "address_bar"];
|
|
610
|
+
async handlePageTransition() {
|
|
611
|
+
const transition = await this.getCurrentTransition();
|
|
612
|
+
if (!transition)
|
|
613
|
+
return;
|
|
614
|
+
const { currentEntry, navigationAction } = transition;
|
|
615
|
+
switch (navigationAction) {
|
|
616
|
+
case "initial":
|
|
617
|
+
// console.log("Initial navigation, no action taken");
|
|
618
|
+
return;
|
|
619
|
+
case "navigate":
|
|
620
|
+
// console.log("transitionType", transition.transitionType);
|
|
621
|
+
// console.log("sending onGoto event", { url: currentEntry.url,
|
|
622
|
+
// type: "navigate", });
|
|
623
|
+
if (this.userInitiatedTransitionTypes.includes(currentEntry.transitionType)) {
|
|
624
|
+
const env = JSON.parse(readFileSync(this.envName, "utf8"));
|
|
625
|
+
const baseUrl = env.baseUrl;
|
|
626
|
+
let url = currentEntry.userTypedURL;
|
|
627
|
+
if (baseUrl && url.startsWith(baseUrl)) {
|
|
628
|
+
url = url.replace(baseUrl, "{{env.baseUrl}}");
|
|
629
|
+
}
|
|
630
|
+
// console.log("User initiated transition");
|
|
631
|
+
this.sendEvent(this.events.onGoto, { url, type: "navigate" });
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
634
|
+
case "back":
|
|
635
|
+
// console.log("User navigated back");
|
|
636
|
+
// console.log("sending onGoto event", {
|
|
637
|
+
// type: "back",
|
|
638
|
+
// });
|
|
639
|
+
this.sendEvent(this.events.onGoto, { type: "back" });
|
|
640
|
+
return;
|
|
641
|
+
case "forward":
|
|
642
|
+
// console.log("User navigated forward"); console.log("sending onGoto event", { type: "forward", });
|
|
643
|
+
this.sendEvent(this.events.onGoto, { type: "forward" });
|
|
644
|
+
return;
|
|
645
|
+
default:
|
|
646
|
+
this.sendEvent(this.events.onGoto, { type: "unknown" });
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
async getCurrentPageTitle() {
|
|
651
|
+
let title = "";
|
|
652
|
+
try {
|
|
653
|
+
title = await this.bvtContext.page.title();
|
|
654
|
+
}
|
|
655
|
+
catch (e) {
|
|
656
|
+
this.logger.error(`Error getting page title: ${getErrorMessage(e)}`);
|
|
657
|
+
}
|
|
658
|
+
return title;
|
|
659
|
+
}
|
|
660
|
+
async getCurrentPageUrl() {
|
|
661
|
+
let url = "";
|
|
662
|
+
try {
|
|
663
|
+
url = await this.bvtContext.page.url();
|
|
664
|
+
}
|
|
665
|
+
catch (e) {
|
|
666
|
+
this.logger.error(`Error getting page url: ${getErrorMessage(e)}`);
|
|
667
|
+
}
|
|
668
|
+
return url;
|
|
669
|
+
}
|
|
670
|
+
_addPagelisteners(context) {
|
|
671
|
+
context.on("page", async (page) => {
|
|
672
|
+
try {
|
|
673
|
+
if (page.isClosed())
|
|
674
|
+
return;
|
|
675
|
+
this.pageSet.add(page);
|
|
676
|
+
await page.waitForLoadState("domcontentloaded");
|
|
677
|
+
// add listener for frame navigation on new tab
|
|
678
|
+
this._addFrameNavigateListener(page);
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
this.logger.error(`Error in page event: ${getErrorMessage(error)}`, undefined, "_addPagelisteners");
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
async openBrowser(_input) {
|
|
686
|
+
const env = JSON.parse(readFileSync(this.envName, "utf8"));
|
|
687
|
+
const url = env.baseUrl;
|
|
688
|
+
await this._initBrowser({ url });
|
|
689
|
+
await this._openTab({ url });
|
|
690
|
+
process.env.TEMP_RUN = "true";
|
|
691
|
+
}
|
|
692
|
+
overlayLocators(event) {
|
|
693
|
+
const locatorsResults = [...(event.locators ?? [])];
|
|
694
|
+
for (const cssLocator of event.cssLocators ?? []) {
|
|
695
|
+
locatorsResults.push({ mode: "NO_TEXT", css: cssLocator });
|
|
696
|
+
}
|
|
697
|
+
if (event.digitLocators) {
|
|
698
|
+
for (const digitLocator of event.digitLocators) {
|
|
699
|
+
locatorsResults.push({ ...digitLocator, mode: "IGNORE_DIGIT" });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (event.contextLocator) {
|
|
703
|
+
locatorsResults.push({
|
|
704
|
+
mode: "CONTEXT",
|
|
705
|
+
text: event.contextLocator.texts?.[0],
|
|
706
|
+
css: event.contextLocator.css,
|
|
707
|
+
climb: event.contextLocator.climbCount,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
return locatorsResults;
|
|
711
|
+
}
|
|
712
|
+
setShouldTakeScreenshot(input) {
|
|
713
|
+
this.shouldTakeScreenshot = input?.value;
|
|
714
|
+
}
|
|
715
|
+
async getScreenShot() {
|
|
716
|
+
const client = await this.context.newCDPSession(this.web.page);
|
|
717
|
+
try {
|
|
718
|
+
// Using CDP to capture the screenshot
|
|
719
|
+
const { data } = await client.send("Page.captureScreenshot", { format: "png" });
|
|
720
|
+
return data;
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
this.logger.error("Error in taking browser screenshot", { error });
|
|
724
|
+
console.error("Error in taking browser screenshot", error);
|
|
725
|
+
}
|
|
726
|
+
finally {
|
|
727
|
+
await client.detach();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
async storeScreenshot(event) {
|
|
731
|
+
try {
|
|
732
|
+
// const spath = path.join(__dirname, "media", `${event.inputID}.png`);
|
|
733
|
+
const screenshotURL = await this.getScreenShot();
|
|
734
|
+
if (!event.element.inputID) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const inputId = event.element.inputID;
|
|
738
|
+
if (!screenshotURL) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
this.screenshotMap.set(inputId, screenshotURL);
|
|
742
|
+
// writeFileSync(spath, screenshotURL, "base64");
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
this.logger.error(`Error in storeScreenshot: ${getErrorMessage(error)}`, undefined, "storeScreenshot");
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
async generateLocators(event) {
|
|
749
|
+
const snapshotDetails = event.snapshotDetails;
|
|
750
|
+
if (!snapshotDetails) {
|
|
751
|
+
throw new Error("No snapshot details found");
|
|
752
|
+
}
|
|
753
|
+
const mode = event.mode;
|
|
754
|
+
const inputID = event.element.inputID;
|
|
755
|
+
const { id, contextId, doc } = snapshotDetails;
|
|
756
|
+
if (!doc) {
|
|
757
|
+
throw new Error("Snapshot details missing document content");
|
|
758
|
+
}
|
|
759
|
+
// const selector = `[data-blinq-id="${id}"]`;
|
|
760
|
+
if (!this.backgroundContext) {
|
|
761
|
+
throw new Error("Background context not initialized");
|
|
762
|
+
}
|
|
763
|
+
const newPage = await this.backgroundContext.newPage();
|
|
764
|
+
const htmlDoc = doc;
|
|
765
|
+
await newPage.setContent(htmlDoc, { waitUntil: "domcontentloaded" });
|
|
766
|
+
const locatorsObj = await newPage.evaluate(([id, contextId, mode]) => {
|
|
767
|
+
const recorder = window.__bvt_Recorder;
|
|
768
|
+
const contextElement = document.querySelector(`[data-blinq-context-id="${contextId}"]`);
|
|
769
|
+
const el = document.querySelector(`[data-blinq-id="${id}"]`);
|
|
770
|
+
if (!recorder || !el) {
|
|
771
|
+
return { locators: [], allStrategyLocators: [] };
|
|
772
|
+
}
|
|
773
|
+
if (contextElement && recorder.locatorGenerator.toContextLocators) {
|
|
774
|
+
const result = recorder.locatorGenerator.toContextLocators(el, contextElement);
|
|
775
|
+
return result ?? { locators: [], allStrategyLocators: [] };
|
|
776
|
+
}
|
|
777
|
+
const isRecordingText = mode === "recordingText";
|
|
778
|
+
return recorder.locatorGenerator.getElementLocators(el, {
|
|
779
|
+
excludeText: isRecordingText,
|
|
780
|
+
});
|
|
781
|
+
}, [id, contextId, mode]);
|
|
782
|
+
// console.log(`Generated locators: for ${inputID}: `, JSON.stringify(locatorsObj));
|
|
783
|
+
await newPage.close();
|
|
784
|
+
if (event.nestFrmLoc?.children) {
|
|
785
|
+
locatorsObj.nestFrmLoc = event.nestFrmLoc.children;
|
|
786
|
+
}
|
|
787
|
+
this.sendEvent(this.events.updateCommand, {
|
|
788
|
+
locators: {
|
|
789
|
+
locators: locatorsObj.locators,
|
|
790
|
+
nestFrmLoc: locatorsObj.nestFrmLoc,
|
|
791
|
+
iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
792
|
+
},
|
|
793
|
+
allStrategyLocators: locatorsObj.allStrategyLocators,
|
|
794
|
+
inputID,
|
|
795
|
+
});
|
|
796
|
+
// const
|
|
797
|
+
}
|
|
798
|
+
async onAction(event) {
|
|
799
|
+
this._updateUrlPath();
|
|
800
|
+
// const locators = this.overlayLocators(event);
|
|
801
|
+
const cmdEvent = {
|
|
802
|
+
...event.element,
|
|
803
|
+
...transformAction(event.action, event.element, event.mode === "recordingText" || event.mode === "recordingContext", !!event.isPopupCloseClick, event.mode === "recordingHover", event.mode === "multiInspecting"),
|
|
804
|
+
// locators: {
|
|
805
|
+
// locators: event.locators,
|
|
806
|
+
// iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
807
|
+
// },
|
|
808
|
+
// allStrategyLocators: event.allStrategyLocators,
|
|
809
|
+
url: event.frame.url,
|
|
810
|
+
title: event.frame.title,
|
|
811
|
+
extract: {},
|
|
812
|
+
lastKnownUrlPath: this.lastKnownUrlPath,
|
|
2249
813
|
};
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
814
|
+
// if (event.nestFrmLoc?.children) {
|
|
815
|
+
// cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
|
|
816
|
+
// }
|
|
817
|
+
// this.logger.info({ event });
|
|
818
|
+
if (this.shouldTakeScreenshot) {
|
|
819
|
+
await this.storeScreenshot(event);
|
|
820
|
+
}
|
|
821
|
+
// this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
822
|
+
// this._updateUrlPath();
|
|
823
|
+
if (event.locators) {
|
|
824
|
+
Object.assign(cmdEvent, {
|
|
825
|
+
locators: {
|
|
826
|
+
locators: event.locators,
|
|
827
|
+
iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
828
|
+
nestFrmLoc: event.nestFrmLoc?.children,
|
|
829
|
+
},
|
|
830
|
+
allStrategyLocators: event.allStrategyLocators,
|
|
831
|
+
});
|
|
832
|
+
this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
833
|
+
this._updateUrlPath();
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
837
|
+
this._updateUrlPath();
|
|
838
|
+
await this.generateLocators(event);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
_updateUrlPath() {
|
|
842
|
+
try {
|
|
843
|
+
const url = this.bvtContext.web.page.url();
|
|
844
|
+
if (url !== "about:blank") {
|
|
845
|
+
this.lastKnownUrlPath = new URL(url).pathname;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
catch (error) {
|
|
849
|
+
this.logger.error("Error in getting last known url path", { error });
|
|
850
|
+
}
|
|
851
|
+
return this.lastKnownUrlPath;
|
|
852
|
+
}
|
|
853
|
+
async closeBrowser(_input) {
|
|
854
|
+
delete process.env.TEMP_RUN;
|
|
855
|
+
await this.watcher?.close();
|
|
856
|
+
this.watcher = null;
|
|
857
|
+
this.previousIndex = null;
|
|
858
|
+
this.previousHistoryLength = null;
|
|
859
|
+
this.previousUrl = null;
|
|
860
|
+
this.previousEntries = null;
|
|
861
|
+
await closeContext();
|
|
862
|
+
this.pageSet.clear();
|
|
863
|
+
}
|
|
864
|
+
async reOpenBrowser(input) {
|
|
865
|
+
if (input && input.envName) {
|
|
866
|
+
this.envName = path.join(this.projectDir, "environments", input.envName + ".json");
|
|
867
|
+
process.env.BLINQ_ENV = this.envName;
|
|
868
|
+
}
|
|
869
|
+
await this.closeBrowser();
|
|
870
|
+
// logger.log("closed");
|
|
871
|
+
await delay(1000);
|
|
872
|
+
await this.openBrowser();
|
|
873
|
+
// logger.log("opened");
|
|
874
|
+
}
|
|
875
|
+
async getNumberOfOccurrences({ searchString, regex = false, partial = true, ignoreCase = false, tag = "*", }) {
|
|
876
|
+
this.isVerify = false;
|
|
877
|
+
//const script = `window.countStringOccurrences(${JSON.stringify(searchString)});`;
|
|
878
|
+
if (searchString.length === 0)
|
|
879
|
+
return -1;
|
|
880
|
+
let result = 0;
|
|
881
|
+
for (let i = 0; i < 3; i++) {
|
|
882
|
+
result = 0;
|
|
883
|
+
try {
|
|
884
|
+
// for (const page of this.context.pages()) {
|
|
885
|
+
const page = this.web.page;
|
|
886
|
+
for (const frame of page.frames()) {
|
|
887
|
+
try {
|
|
888
|
+
//scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params: Params)
|
|
889
|
+
const frameResult = await this.web._locateElementByText(frame, searchString, tag, regex, partial, ignoreCase, {});
|
|
890
|
+
result += frameResult.elementCount;
|
|
891
|
+
}
|
|
892
|
+
catch (e) {
|
|
893
|
+
console.log(e);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
// }
|
|
897
|
+
return result;
|
|
898
|
+
}
|
|
899
|
+
catch (e) {
|
|
900
|
+
console.log(e);
|
|
901
|
+
result = 0;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
async startRecordingInput(_input) {
|
|
906
|
+
await this.setMode("recordingInput");
|
|
907
|
+
}
|
|
908
|
+
async stopRecordingInput(_input) {
|
|
909
|
+
await this.setMode("idle");
|
|
910
|
+
}
|
|
911
|
+
async startRecordingText(input) {
|
|
912
|
+
const isInspectMode = typeof input === "boolean" ? input : !!input?.isInspectMode;
|
|
913
|
+
if (isInspectMode) {
|
|
914
|
+
await this.setMode("inspecting");
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
await this.setMode("recordingText");
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
async stopRecordingText(_input) {
|
|
921
|
+
await this.setMode("idle");
|
|
922
|
+
}
|
|
923
|
+
async startRecordingContext(_input) {
|
|
924
|
+
await this.setMode("recordingContext");
|
|
925
|
+
}
|
|
926
|
+
async stopRecordingContext(_input) {
|
|
927
|
+
await this.setMode("idle");
|
|
928
|
+
}
|
|
929
|
+
async abortExecution() {
|
|
930
|
+
await this.stepRunner.abortExecution();
|
|
931
|
+
}
|
|
932
|
+
async pauseExecution({ cmdId }) {
|
|
933
|
+
await this.stepRunner.pauseExecution(cmdId);
|
|
934
|
+
}
|
|
935
|
+
async resumeExecution({ cmdId }) {
|
|
936
|
+
await this.stepRunner.resumeExecution(cmdId);
|
|
937
|
+
}
|
|
938
|
+
async dealyedRevertMode() {
|
|
939
|
+
const timerId = setTimeout(async () => {
|
|
940
|
+
await this.revertMode();
|
|
941
|
+
}, 100);
|
|
942
|
+
this.timerId = timerId;
|
|
943
|
+
}
|
|
944
|
+
async runStep({ step, parametersMap, tags, isFirstStep = false, listenNetwork = false, AICode }, options) {
|
|
945
|
+
const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
|
|
946
|
+
const env = path.basename(this.envName, ".json");
|
|
947
|
+
const envVars = {
|
|
948
|
+
TOKEN: this.TOKEN,
|
|
949
|
+
TEMP_RUN: "true",
|
|
950
|
+
REPORT_FOLDER: this.bvtContext.reportFolder,
|
|
951
|
+
BLINQ_ENV: this.envName,
|
|
952
|
+
DEBUG: "blinq:route",
|
|
2265
953
|
};
|
|
2266
|
-
|
|
2267
|
-
|
|
954
|
+
if (!step.isImplemented) {
|
|
955
|
+
envVars.BVT_TEMP_SNAPSHOTS_FOLDER = path.join(this.tempSnapshotsFolder, env);
|
|
956
|
+
}
|
|
957
|
+
this.bvtContext.navigate = true;
|
|
958
|
+
this.bvtContext.loadedRoutes = null;
|
|
959
|
+
this.bvtContext.STORE_DETAILED_NETWORK_DATA = !!listenNetwork;
|
|
960
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
961
|
+
process.env[key] = value;
|
|
962
|
+
}
|
|
963
|
+
if (this.timerId) {
|
|
964
|
+
clearTimeout(this.timerId);
|
|
965
|
+
this.timerId = null;
|
|
966
|
+
}
|
|
967
|
+
await this.setMode("running");
|
|
2268
968
|
try {
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
969
|
+
step.text = step.text.trim();
|
|
970
|
+
const { result, info } = await this.stepRunner.runStep({
|
|
971
|
+
step,
|
|
972
|
+
parametersMap,
|
|
973
|
+
envPath: this.envName,
|
|
974
|
+
tags,
|
|
975
|
+
config: this.config,
|
|
976
|
+
AICode,
|
|
977
|
+
}, this.bvtContext, {
|
|
978
|
+
skipAfter,
|
|
979
|
+
skipBefore,
|
|
980
|
+
});
|
|
981
|
+
await this.revertMode();
|
|
982
|
+
return { info };
|
|
983
|
+
}
|
|
984
|
+
catch (error) {
|
|
985
|
+
await this.revertMode();
|
|
986
|
+
throw error;
|
|
987
|
+
}
|
|
988
|
+
finally {
|
|
989
|
+
for (const key of Object.keys(envVars)) {
|
|
990
|
+
delete process.env[key];
|
|
991
|
+
}
|
|
992
|
+
this.bvtContext.navigate = false;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env, AICode, }) {
|
|
996
|
+
const res = await this.workspaceService.saveScenario({
|
|
997
|
+
scenario,
|
|
998
|
+
featureName,
|
|
999
|
+
override,
|
|
1000
|
+
isSingleStep,
|
|
1001
|
+
branch,
|
|
1002
|
+
isEditing,
|
|
1003
|
+
projectId: path.basename(this.projectDir),
|
|
1004
|
+
env: env ?? this.envName,
|
|
1005
|
+
AICode,
|
|
1006
|
+
});
|
|
1007
|
+
if (res.success) {
|
|
1008
|
+
await this.cleanup({ tags: scenario.tags });
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
throw new Error(res.message || "Error saving scenario");
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
async getImplementedSteps(_input) {
|
|
1015
|
+
const stepsAndScenarios = await getImplementedSteps(this.projectDir);
|
|
1016
|
+
const implementedSteps = stepsAndScenarios.implementedSteps;
|
|
1017
|
+
const scenarios = stepsAndScenarios.scenarios;
|
|
1018
|
+
for (const scenario of scenarios) {
|
|
1019
|
+
this.scenariosStepsMap.set(scenario.name, scenario.steps);
|
|
1020
|
+
delete scenario.steps;
|
|
2272
1021
|
}
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
1022
|
+
return {
|
|
1023
|
+
implementedSteps,
|
|
1024
|
+
scenarios,
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
async getStepsAndCommandsForScenario({ name, featureName }) {
|
|
1028
|
+
const steps = this.scenariosStepsMap.get(name) || [];
|
|
1029
|
+
for (const step of steps) {
|
|
1030
|
+
if (step.isImplemented) {
|
|
1031
|
+
step.commands = this.getCommandsForImplementedStep({ stepName: step.text });
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
step.commands = [];
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return steps;
|
|
1038
|
+
// return getStepsAndCommandsForScenario({
|
|
1039
|
+
// name,
|
|
1040
|
+
// featureName,
|
|
1041
|
+
// projectDir: this.projectDir,
|
|
1042
|
+
// map: this.scenariosStepsMap,
|
|
1043
|
+
// });
|
|
1044
|
+
}
|
|
1045
|
+
async generateStepName({ commands, stepsNames, parameters, map, }) {
|
|
1046
|
+
return await this.namesService.generateStepName({ commands, stepsNames, parameters, map });
|
|
1047
|
+
}
|
|
1048
|
+
async generateScenarioAndFeatureNames(scenarioAsText) {
|
|
1049
|
+
return await this.namesService.generateScenarioAndFeatureNames(scenarioAsText);
|
|
1050
|
+
}
|
|
1051
|
+
async generateCommandName({ command }) {
|
|
1052
|
+
return await this.namesService.generateCommandName({ command });
|
|
1053
|
+
}
|
|
1054
|
+
async getCurrentChromiumPath() {
|
|
1055
|
+
const currentURL = await this.bvtContext.web.page.url();
|
|
1056
|
+
const env = JSON.parse(readFileSync(this.envName, "utf8"));
|
|
1057
|
+
const baseURL = env.baseUrl;
|
|
1058
|
+
const relativeURL = currentURL.startsWith(baseURL) ? currentURL.replace(baseURL, "/") : undefined;
|
|
1059
|
+
return {
|
|
1060
|
+
relativeURL,
|
|
1061
|
+
baseURL,
|
|
1062
|
+
currentURL,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
getReportFolder() {
|
|
1066
|
+
if (this.bvtContext.reportFolder) {
|
|
1067
|
+
return this.bvtContext.reportFolder;
|
|
1068
|
+
}
|
|
1069
|
+
else
|
|
1070
|
+
return "";
|
|
1071
|
+
}
|
|
1072
|
+
getSnapshotFolder() {
|
|
1073
|
+
if (this.bvtContext.snapshotFolder) {
|
|
1074
|
+
return path.join(process.cwd(), this.bvtContext.snapshotFolder);
|
|
1075
|
+
}
|
|
1076
|
+
else
|
|
1077
|
+
return "";
|
|
1078
|
+
}
|
|
1079
|
+
async overwriteTestData(data) {
|
|
1080
|
+
this.bvtContext.stable?.overwriteTestData(data.value, this.world);
|
|
1081
|
+
}
|
|
1082
|
+
_watchTestData() {
|
|
1083
|
+
this.watcher = chokidar.watch(_getDataFile(this.world, this.bvtContext, this.web), {
|
|
1084
|
+
persistent: true,
|
|
1085
|
+
ignoreInitial: true,
|
|
1086
|
+
awaitWriteFinish: {
|
|
1087
|
+
stabilityThreshold: 2000,
|
|
1088
|
+
pollInterval: 100,
|
|
1089
|
+
},
|
|
1090
|
+
});
|
|
1091
|
+
if (existsSync(_getDataFile(this.world, this.bvtContext, this.web))) {
|
|
2276
1092
|
try {
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
1093
|
+
const testData = JSON.parse(readFileSync(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
|
|
1094
|
+
// this.logger.info("Test data", testData);
|
|
1095
|
+
this.sendEvent(this.events.getTestData, testData);
|
|
2280
1096
|
}
|
|
2281
|
-
|
|
2282
|
-
|
|
1097
|
+
catch (e) {
|
|
1098
|
+
// this.logger.error("Error reading test data file", e);
|
|
1099
|
+
console.log("Error reading test data file", e);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
this.logger.info("Watching for test data changes");
|
|
1103
|
+
this.watcher.on("all", async (_event, _path) => {
|
|
2283
1104
|
try {
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
1105
|
+
const testData = JSON.parse(await readFile(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
|
|
1106
|
+
// this.logger.info("Test data", testData);
|
|
1107
|
+
console.log("Test data changed", testData);
|
|
1108
|
+
this.sendEvent(this.events.getTestData, testData);
|
|
1109
|
+
}
|
|
1110
|
+
catch (e) {
|
|
1111
|
+
// this.logger.error("Error reading test data file", e);
|
|
1112
|
+
console.log("Error reading test data file", e);
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
async loadTestData({ data, type }) {
|
|
1117
|
+
if (type === "user") {
|
|
1118
|
+
const username = data.username;
|
|
1119
|
+
await this.web.loadTestDataAsync("users", username, this.world);
|
|
1120
|
+
}
|
|
1121
|
+
else {
|
|
1122
|
+
const csv = data.csv;
|
|
1123
|
+
const row = data.row;
|
|
1124
|
+
// code = `await context.web.loadTestDataAsync("csv","${csv}:${row}", this)`;
|
|
1125
|
+
await this.web.loadTestDataAsync("csv", `${csv}:${row}`, this.world);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
async discardTestData({ tags }) {
|
|
1129
|
+
resetTestData(this.envName, this.world);
|
|
1130
|
+
await this.cleanup({ tags });
|
|
1131
|
+
}
|
|
1132
|
+
async addToTestData(obj) {
|
|
1133
|
+
if (!existsSync(_getDataFile(this.world, this.bvtContext, this.web))) {
|
|
1134
|
+
await writeFile(_getDataFile(this.world, this.bvtContext, this.web), JSON.stringify({}), "utf8");
|
|
1135
|
+
}
|
|
1136
|
+
let data = JSON.parse(await readFile(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
|
|
1137
|
+
data = Object.assign(data, obj);
|
|
1138
|
+
await writeFile(_getDataFile(this.world, this.bvtContext, this.web), JSON.stringify(data), "utf8");
|
|
1139
|
+
}
|
|
1140
|
+
getScenarios() {
|
|
1141
|
+
const featureFiles = readdirSync(path.join(this.projectDir, "features"))
|
|
1142
|
+
.filter((file) => file.endsWith(".feature"))
|
|
1143
|
+
.map((file) => path.join(this.projectDir, "features", file));
|
|
1144
|
+
try {
|
|
1145
|
+
const parsedFiles = featureFiles.map((file) => this.parseFeatureFile(file));
|
|
1146
|
+
const output = {};
|
|
1147
|
+
parsedFiles.forEach((file) => {
|
|
1148
|
+
if (!file.feature)
|
|
1149
|
+
return;
|
|
1150
|
+
if (!file.feature.name)
|
|
1151
|
+
return;
|
|
1152
|
+
output[file.feature.name] = [];
|
|
1153
|
+
file.feature.children.forEach((child) => {
|
|
1154
|
+
if (child.scenario) {
|
|
1155
|
+
output[file.feature.name].push(child.scenario.name);
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
});
|
|
1159
|
+
return output;
|
|
1160
|
+
}
|
|
1161
|
+
catch (e) {
|
|
1162
|
+
console.log(e);
|
|
1163
|
+
}
|
|
1164
|
+
return {};
|
|
1165
|
+
}
|
|
1166
|
+
getCommandsForImplementedStep({ stepName }) {
|
|
1167
|
+
const step_definitions = loadStepDefinitions(this.projectDir);
|
|
1168
|
+
const stepParams = parseStepTextParameters(stepName);
|
|
1169
|
+
return getCommandsForImplementedStep(stepName, step_definitions, stepParams).commands;
|
|
1170
|
+
}
|
|
1171
|
+
loadExistingScenario({ featureName, scenarioName }) {
|
|
1172
|
+
const step_definitions = loadStepDefinitions(this.projectDir);
|
|
1173
|
+
const featureFilePath = path.join(this.projectDir, "features", featureName);
|
|
1174
|
+
const gherkinDoc = this.parseFeatureFile(featureFilePath);
|
|
1175
|
+
const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
|
|
1176
|
+
this.scenarioDoc = scenario;
|
|
1177
|
+
const steps = [];
|
|
1178
|
+
const parameters = [];
|
|
1179
|
+
const datasets = [];
|
|
1180
|
+
if (scenario.examples && scenario.examples.length > 0) {
|
|
1181
|
+
const example = scenario.examples[0];
|
|
1182
|
+
example?.tableHeader?.cells.forEach((cell, index) => {
|
|
1183
|
+
parameters.push({
|
|
1184
|
+
key: cell.value,
|
|
1185
|
+
value: unEscapeNonPrintables(example.tableBody[0].cells[index].value),
|
|
1186
|
+
});
|
|
1187
|
+
// datasets.push({
|
|
1188
|
+
// data: example.tableBody[]
|
|
1189
|
+
// })
|
|
1190
|
+
});
|
|
1191
|
+
for (let i = 0; i < example.tableBody.length; i++) {
|
|
1192
|
+
const row = example.tableBody[i];
|
|
1193
|
+
// for (const row of example.tableBody) {
|
|
1194
|
+
const paramters = [];
|
|
1195
|
+
row.cells.forEach((cell, index) => {
|
|
1196
|
+
paramters.push({
|
|
1197
|
+
key: example.tableHeader.cells[index].value,
|
|
1198
|
+
value: unEscapeNonPrintables(cell.value),
|
|
1199
|
+
});
|
|
1200
|
+
});
|
|
1201
|
+
datasets.push({
|
|
1202
|
+
data: paramters,
|
|
1203
|
+
datasetId: i,
|
|
1204
|
+
});
|
|
2287
1205
|
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
1206
|
+
}
|
|
1207
|
+
for (const step of scenario.steps) {
|
|
1208
|
+
const stepParams = parseStepTextParameters(step.text);
|
|
1209
|
+
// console.log("Parsing step ", step, stepParams);
|
|
1210
|
+
const _s = getCommandsForImplementedStep(step.text, step_definitions, stepParams);
|
|
1211
|
+
delete step.location;
|
|
1212
|
+
const _step = {
|
|
1213
|
+
...step,
|
|
1214
|
+
..._s,
|
|
1215
|
+
keyword: step.keyword.trim(),
|
|
1216
|
+
};
|
|
1217
|
+
parseRouteFiles(this.projectDir, _step);
|
|
1218
|
+
steps.push(_step);
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
name: scenario.name,
|
|
1222
|
+
tags: scenario.tags.map((tag) => tag.name),
|
|
1223
|
+
steps,
|
|
1224
|
+
parameters,
|
|
1225
|
+
datasets,
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
async findRelatedTextInAllFrames({ searchString, climb, contextText, params, }) {
|
|
1229
|
+
if (searchString.length === 0)
|
|
1230
|
+
return -1;
|
|
1231
|
+
let result = 0;
|
|
1232
|
+
for (let i = 0; i < 3; i++) {
|
|
1233
|
+
result = 0;
|
|
1234
|
+
try {
|
|
2293
1235
|
try {
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
1236
|
+
const allFrameResult = await this.web.findRelatedTextInAllFrames(contextText, climb, searchString, params, {}, this.world);
|
|
1237
|
+
for (const frameResult of allFrameResult) {
|
|
1238
|
+
result += frameResult.elementCount;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
catch (e) {
|
|
1242
|
+
console.log(e);
|
|
2297
1243
|
}
|
|
2298
|
-
|
|
1244
|
+
return result;
|
|
1245
|
+
}
|
|
1246
|
+
catch (e) {
|
|
1247
|
+
console.log(e);
|
|
1248
|
+
result = 0;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return result;
|
|
1252
|
+
}
|
|
1253
|
+
async setStepCodeByScenario({ function_name, mjs_file_content, user_request, selectedTarget, page_context, AIMemory, steps_context, }) {
|
|
1254
|
+
const runsURL = getRunsServiceBaseURL();
|
|
1255
|
+
const url = `${runsURL}/process-user-request/generate-code-with-context`;
|
|
1256
|
+
try {
|
|
1257
|
+
const result = await axiosClient({
|
|
1258
|
+
url,
|
|
1259
|
+
method: "POST",
|
|
1260
|
+
data: {
|
|
1261
|
+
function_name,
|
|
1262
|
+
mjs_file_content,
|
|
1263
|
+
user_request,
|
|
1264
|
+
selectedTarget,
|
|
1265
|
+
page_context,
|
|
1266
|
+
AIMemory,
|
|
1267
|
+
steps_context,
|
|
1268
|
+
},
|
|
1269
|
+
headers: {
|
|
1270
|
+
Authorization: `Bearer ${this.TOKEN}`,
|
|
1271
|
+
"X-Source": "recorder",
|
|
1272
|
+
},
|
|
1273
|
+
});
|
|
1274
|
+
if (result.status !== 200) {
|
|
1275
|
+
return { success: false, message: "Error while fetching code changes" };
|
|
2299
1276
|
}
|
|
2300
|
-
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
1277
|
+
return { success: true, data: result.data };
|
|
1278
|
+
}
|
|
1279
|
+
catch (error) {
|
|
1280
|
+
// @ts-ignore
|
|
1281
|
+
const reason = error?.response?.data?.error || "";
|
|
1282
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1283
|
+
throw new Error(`Failed to fetch code changes: ${errorMessage} \n ${reason}`);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
async getStepCodeByScenario({ featureName, scenarioName, projectId, branch, }) {
|
|
1287
|
+
try {
|
|
1288
|
+
const runsURL = getRunsServiceBaseURL();
|
|
1289
|
+
const ssoURL = runsURL.replace("/runs", "/auth");
|
|
1290
|
+
const privateRepoURL = `${ssoURL}/isRepoPrivate?project_id=${projectId}`;
|
|
1291
|
+
const isPrivateRepoReq = await axiosClient({
|
|
1292
|
+
url: privateRepoURL,
|
|
1293
|
+
method: "GET",
|
|
1294
|
+
headers: {
|
|
1295
|
+
Authorization: `Bearer ${this.TOKEN}`,
|
|
1296
|
+
"X-Source": "recorder",
|
|
1297
|
+
},
|
|
2315
1298
|
});
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
console.warn("Clipboard bridge failed to dispatch synthetic paste event", error);
|
|
2319
|
-
}
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
if (pasteHandled) {
|
|
2323
|
-
return;
|
|
2324
|
-
}
|
|
2325
|
-
|
|
2326
|
-
const callLegacyExecCommand = (command, value) => {
|
|
2327
|
-
const execCommand = document && document["execCommand"];
|
|
2328
|
-
if (typeof execCommand === "function") {
|
|
2329
|
-
try {
|
|
2330
|
-
return execCommand.call(document, command, false, value);
|
|
2331
|
-
} catch (error) {
|
|
2332
|
-
console.warn("Clipboard bridge failed to execute legacy command", error);
|
|
1299
|
+
if (isPrivateRepoReq.status !== 200) {
|
|
1300
|
+
return { success: false, message: "Error while checking repo privacy" };
|
|
2333
1301
|
}
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
1302
|
+
const isPrivateRepo = isPrivateRepoReq.data.isPrivate ? isPrivateRepoReq.data.isPrivate : false;
|
|
1303
|
+
const workspaceURL = runsURL.replace("/runs", "/workspace");
|
|
1304
|
+
const url = `${workspaceURL}/get-step-code-by-scenario`;
|
|
1305
|
+
const result = await axiosClient({
|
|
1306
|
+
url,
|
|
1307
|
+
method: "POST",
|
|
1308
|
+
data: {
|
|
1309
|
+
scenarioName,
|
|
1310
|
+
featureName,
|
|
1311
|
+
projectId,
|
|
1312
|
+
isPrivateRepo,
|
|
1313
|
+
branch,
|
|
1314
|
+
},
|
|
1315
|
+
headers: {
|
|
1316
|
+
Authorization: `Bearer ${this.TOKEN}`,
|
|
1317
|
+
"X-Source": "recorder",
|
|
1318
|
+
},
|
|
1319
|
+
});
|
|
1320
|
+
if (result.status !== 200) {
|
|
1321
|
+
return { success: false, message: "Error while getting step code" };
|
|
1322
|
+
}
|
|
1323
|
+
return { success: true, data: result.data.stepInfo };
|
|
1324
|
+
}
|
|
1325
|
+
catch (error) {
|
|
1326
|
+
const axiosError = error;
|
|
1327
|
+
const reason = axiosError?.response?.data?.error || "";
|
|
1328
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1329
|
+
throw new Error(`Failed to get step code: ${errorMessage} \n ${reason}`);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
async getContext() {
|
|
1333
|
+
await this.page.waitForLoadState("domcontentloaded");
|
|
1334
|
+
await this.page.waitForSelector("body");
|
|
1335
|
+
await this.page.waitForTimeout(500);
|
|
1336
|
+
return await this.page.evaluate(() => {
|
|
1337
|
+
return document.documentElement.outerHTML;
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
async deleteCommandFromStepCode({ scenario, AICode, command }) {
|
|
1341
|
+
if (!AICode || AICode.length === 0) {
|
|
1342
|
+
console.log("No AI code available to delete.");
|
|
2341
1343
|
return;
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
1344
|
+
}
|
|
1345
|
+
const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
|
|
1346
|
+
const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
|
|
1347
|
+
process.env.tempFeaturesFolderPath = __temp_features_FolderName;
|
|
1348
|
+
process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
|
|
1349
|
+
try {
|
|
1350
|
+
await this.stepRunner.copyCodetoTempFolder({ tempFolderPath, AICode });
|
|
1351
|
+
await this.stepRunner.writeWrapperCode(tempFolderPath);
|
|
1352
|
+
const codeView = AICode.find((f) => f.stepName === scenario.step.text);
|
|
1353
|
+
if (!codeView) {
|
|
1354
|
+
throw new Error("Step code not found for step: " + scenario.step.text);
|
|
2352
1355
|
}
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
1356
|
+
const functionName = codeView.functionName;
|
|
1357
|
+
const mjsPath = path
|
|
1358
|
+
.normalize(codeView.mjsFile)
|
|
1359
|
+
.split(path.sep)
|
|
1360
|
+
.filter((part) => part !== "features")
|
|
1361
|
+
.join(path.sep);
|
|
1362
|
+
const codePath = path.join(tempFolderPath, mjsPath);
|
|
1363
|
+
if (!existsSync(codePath)) {
|
|
1364
|
+
throw new Error("Step code file not found: " + codePath);
|
|
1365
|
+
}
|
|
1366
|
+
const codePage = getCodePage(codePath);
|
|
1367
|
+
const elements = codePage.getVariableDeclarationAsObject("elements");
|
|
1368
|
+
const cucumberStep = getCucumberStep({ step: scenario.step });
|
|
1369
|
+
cucumberStep.text = scenario.step.text;
|
|
1370
|
+
const stepCommands = scenario.step.commands;
|
|
1371
|
+
const cmd = _toRecordingStep(command, scenario.step.name);
|
|
1372
|
+
const recording = new Recording();
|
|
1373
|
+
recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
|
|
1374
|
+
const step = { ...(recording.steps[0] ?? {}), ...(cmd ?? {}) };
|
|
1375
|
+
const result = _generateCodeFromCommand(step, elements, {});
|
|
1376
|
+
codePage._removeCommands(functionName, result.codeLines);
|
|
1377
|
+
codePage.removeUnusedElements();
|
|
1378
|
+
codePage.save();
|
|
1379
|
+
await rm(tempFolderPath, { recursive: true, force: true });
|
|
1380
|
+
return { code: codePage.fileContent, mjsFile: codeView.mjsFile };
|
|
1381
|
+
}
|
|
1382
|
+
catch (error) {
|
|
1383
|
+
await rm(tempFolderPath, { recursive: true, force: true });
|
|
1384
|
+
throw error;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
async addCommandToStepCode({ scenario, AICode }) {
|
|
1388
|
+
if (!AICode || AICode.length === 0) {
|
|
1389
|
+
console.log("No AI code available to add.");
|
|
2361
1390
|
return;
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
1391
|
+
}
|
|
1392
|
+
const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
|
|
1393
|
+
const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
|
|
1394
|
+
process.env.tempFeaturesFolderPath = __temp_features_FolderName;
|
|
1395
|
+
process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
|
|
1396
|
+
try {
|
|
1397
|
+
await this.stepRunner.copyCodetoTempFolder({ tempFolderPath, AICode });
|
|
1398
|
+
await this.stepRunner.writeWrapperCode(tempFolderPath);
|
|
1399
|
+
let codeView = AICode.find((f) => f.stepName === scenario.step.text);
|
|
1400
|
+
if (codeView) {
|
|
1401
|
+
scenario.step.commands = [scenario.step.commands.pop()];
|
|
1402
|
+
const functionName = codeView.functionName;
|
|
1403
|
+
const mjsPath = path
|
|
1404
|
+
.normalize(codeView.mjsFile)
|
|
1405
|
+
.split(path.sep)
|
|
1406
|
+
.filter((part) => part !== "features")
|
|
1407
|
+
.join(path.sep);
|
|
1408
|
+
const codePath = path.join(tempFolderPath, mjsPath);
|
|
1409
|
+
if (!existsSync(codePath)) {
|
|
1410
|
+
throw new Error("Step code file not found: " + codePath);
|
|
1411
|
+
}
|
|
1412
|
+
const codePage = getCodePage(codePath);
|
|
1413
|
+
const elements = codePage.getVariableDeclarationAsObject("elements");
|
|
1414
|
+
const cucumberStep = getCucumberStep({ step: scenario.step });
|
|
1415
|
+
cucumberStep.text = scenario.step.text;
|
|
1416
|
+
const stepCommands = scenario.step.commands;
|
|
1417
|
+
const cmd = _toRecordingStep(scenario.step.commands[0], scenario.step.name);
|
|
1418
|
+
const recording = new Recording();
|
|
1419
|
+
recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
|
|
1420
|
+
const step = { ...(recording.steps[0] ?? {}), ...(cmd ?? {}) };
|
|
1421
|
+
const result = _generateCodeFromCommand(step, elements, {});
|
|
1422
|
+
codePage.insertElements(result.elements);
|
|
1423
|
+
codePage._injectOneCommand(functionName, result.codeLines.join("\n"));
|
|
1424
|
+
codePage.save();
|
|
1425
|
+
await rm(tempFolderPath, { recursive: true, force: true });
|
|
1426
|
+
return { code: codePage.fileContent, newStep: false, mjsFile: codeView.mjsFile };
|
|
2371
1427
|
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
const
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
|
|
2385
|
-
const caret = start + text.length;
|
|
2386
|
-
if (typeof input.setSelectionRange === "function") {
|
|
2387
|
-
input.setSelectionRange(caret, caret);
|
|
1428
|
+
console.log("Step code not found for step: ", scenario.step.text);
|
|
1429
|
+
codeView = AICode[0];
|
|
1430
|
+
const functionName = toMethodName(scenario.step.text);
|
|
1431
|
+
const codeLines = [];
|
|
1432
|
+
const mjsPath = path
|
|
1433
|
+
.normalize(codeView.mjsFile)
|
|
1434
|
+
.split(path.sep)
|
|
1435
|
+
.filter((part) => part !== "features")
|
|
1436
|
+
.join(path.sep);
|
|
1437
|
+
const codePath = path.join(tempFolderPath, mjsPath);
|
|
1438
|
+
if (!existsSync(codePath)) {
|
|
1439
|
+
throw new Error("Step code file not found: " + codePath);
|
|
2388
1440
|
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
1441
|
+
const codePage = getCodePage(codePath);
|
|
1442
|
+
const elements = codePage.getVariableDeclarationAsObject("elements") || {};
|
|
1443
|
+
let newElements = { ...elements };
|
|
1444
|
+
const cucumberStep = getCucumberStep({ step: scenario.step });
|
|
1445
|
+
cucumberStep.text = scenario.step.text;
|
|
1446
|
+
const stepCommands = scenario.step.commands;
|
|
1447
|
+
stepCommands.forEach((command) => {
|
|
1448
|
+
const cmd = _toRecordingStep(command, scenario.step.name);
|
|
1449
|
+
const recording = new Recording();
|
|
1450
|
+
recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
|
|
1451
|
+
const step = { ...(recording.steps[0] ?? {}), ...(cmd ?? {}) };
|
|
1452
|
+
const result = _generateCodeFromCommand(step, elements, {});
|
|
1453
|
+
const elementUpdates = result.elements ?? {};
|
|
1454
|
+
newElements = { ...elementUpdates };
|
|
1455
|
+
codeLines.push(...(result.codeLines ?? []));
|
|
1456
|
+
});
|
|
1457
|
+
codePage.insertElements(newElements);
|
|
1458
|
+
codePage.addInfraCommand(functionName, cucumberStep.text, cucumberStep.getVariablesList(), codeLines, false, "recorder");
|
|
1459
|
+
const keyword = (cucumberStep.keywordAlias ?? cucumberStep.keyword).trim();
|
|
1460
|
+
codePage.addCucumberStep(keyword, cucumberStep.getTemplate(), functionName, stepCommands.length);
|
|
1461
|
+
codePage.save();
|
|
1462
|
+
await rm(tempFolderPath, { recursive: true, force: true });
|
|
1463
|
+
return { code: codePage.fileContent, newStep: true, functionName, mjsFile: codeView.mjsFile };
|
|
1464
|
+
}
|
|
1465
|
+
catch (error) {
|
|
1466
|
+
await rm(tempFolderPath, { recursive: true, force: true });
|
|
1467
|
+
throw error;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
async cleanup({ tags }) {
|
|
1471
|
+
const noopStep = {
|
|
1472
|
+
text: "Noop",
|
|
1473
|
+
isImplemented: true,
|
|
1474
|
+
};
|
|
1475
|
+
const projectDir = this.projectDir;
|
|
1476
|
+
console.log("Cleaning up project dir:", projectDir);
|
|
1477
|
+
try {
|
|
1478
|
+
// run a dummy scenario that will run after hooks
|
|
1479
|
+
await this.runStep({
|
|
1480
|
+
step: noopStep,
|
|
1481
|
+
parametersMap: {},
|
|
1482
|
+
tags: tags || [],
|
|
1483
|
+
}, {
|
|
1484
|
+
skipAfter: false,
|
|
1485
|
+
});
|
|
1486
|
+
// delete the temp folders (any folder that starts with __temp_features)
|
|
1487
|
+
const tempFolders = readdirSync(projectDir).filter((folder) => folder.startsWith("__temp_features"));
|
|
1488
|
+
for (const folder of tempFolders) {
|
|
1489
|
+
const folderPath = path.join(projectDir, folder);
|
|
1490
|
+
if (existsSync(folderPath)) {
|
|
1491
|
+
this.logger.info(`Deleting temp folder: ${folderPath}`);
|
|
1492
|
+
rmSync(folderPath, { recursive: true });
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
catch (error) {
|
|
1497
|
+
console.error("Error in cleanup", error);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
async processAriaSnapshot(snapshot) {
|
|
1501
|
+
try {
|
|
1502
|
+
await this.evaluateInAllFrames(this.context, `window.__bvt_Recorder.processAriaSnapshot(${JSON.stringify(snapshot)});`);
|
|
1503
|
+
return true;
|
|
1504
|
+
}
|
|
1505
|
+
catch (e) {
|
|
1506
|
+
return false;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
async deselectAriaElements() {
|
|
1510
|
+
try {
|
|
1511
|
+
await this.evaluateInAllFrames(this.context, `window.__bvt_Recorder.deselectAriaElements();`);
|
|
1512
|
+
return true;
|
|
1513
|
+
}
|
|
1514
|
+
catch (e) {
|
|
1515
|
+
return false;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
async initExecution({ tags = [] }) {
|
|
1519
|
+
// run before hooks
|
|
1520
|
+
const noopStep = {
|
|
1521
|
+
text: "Noop",
|
|
1522
|
+
isImplemented: true,
|
|
1523
|
+
};
|
|
1524
|
+
await this.runStep({
|
|
1525
|
+
step: noopStep,
|
|
1526
|
+
parametersMap: {},
|
|
1527
|
+
tags,
|
|
1528
|
+
}, {
|
|
1529
|
+
skipBefore: false,
|
|
1530
|
+
skipAfter: true,
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
async cleanupExecution({ tags = [] }) {
|
|
1534
|
+
// run after hooks
|
|
1535
|
+
const noopStep = {
|
|
1536
|
+
text: "Noop",
|
|
1537
|
+
isImplemented: true,
|
|
1538
|
+
};
|
|
1539
|
+
await this.runStep({
|
|
1540
|
+
step: noopStep,
|
|
1541
|
+
parametersMap: {},
|
|
1542
|
+
tags,
|
|
1543
|
+
}, {
|
|
1544
|
+
skipBefore: true,
|
|
1545
|
+
skipAfter: false,
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
async resetExecution({ tags = [] }) {
|
|
1549
|
+
// run after hooks followed by before hooks
|
|
1550
|
+
await this.cleanupExecution({ tags });
|
|
1551
|
+
await this.initExecution({ tags });
|
|
1552
|
+
}
|
|
1553
|
+
parseFeatureFile(featureFilePath) {
|
|
1554
|
+
try {
|
|
1555
|
+
let id = 0;
|
|
1556
|
+
const uuidFn = () => (++id).toString(16);
|
|
1557
|
+
const builder = new AstBuilder(uuidFn);
|
|
1558
|
+
const matcher = new GherkinClassicTokenMatcher();
|
|
1559
|
+
const parser = new Parser(builder, matcher);
|
|
1560
|
+
const source = readFileSync(featureFilePath, "utf8");
|
|
1561
|
+
const gherkinDocument = parser.parse(source);
|
|
1562
|
+
return gherkinDocument;
|
|
1563
|
+
}
|
|
1564
|
+
catch (e) {
|
|
1565
|
+
this.logger.error(`Error parsing feature file: ${featureFilePath}`, { error: e });
|
|
1566
|
+
console.log(e);
|
|
1567
|
+
}
|
|
1568
|
+
return {};
|
|
1569
|
+
}
|
|
1570
|
+
stopRecordingNetwork(_input) {
|
|
1571
|
+
if (this.bvtContext) {
|
|
1572
|
+
this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
async fakeParams(params) {
|
|
1576
|
+
const newFakeParams = {};
|
|
1577
|
+
Object.entries(params).forEach(([key, rawValue]) => {
|
|
1578
|
+
if (!rawValue.startsWith("{{") || !rawValue.endsWith("}}")) {
|
|
1579
|
+
newFakeParams[key] = rawValue;
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
try {
|
|
1583
|
+
newFakeParams[key] = faker.helpers.fake(rawValue);
|
|
1584
|
+
}
|
|
1585
|
+
catch {
|
|
1586
|
+
newFakeParams[key] = rawValue;
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1589
|
+
return newFakeParams;
|
|
1590
|
+
}
|
|
2487
1591
|
}
|