@dev-blinq/cucumber_client 1.0.1475-dev → 1.0.1475-stage

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +49 -49
  2. package/bin/assets/scripts/recorder.js +87 -34
  3. package/bin/assets/scripts/snapshot_capturer.js +10 -17
  4. package/bin/assets/scripts/unique_locators.js +78 -28
  5. package/bin/assets/templates/_hooks_template.txt +6 -2
  6. package/bin/assets/templates/utils_template.txt +16 -16
  7. package/bin/client/code_cleanup/codemod/find_harcoded_locators.js +173 -0
  8. package/bin/client/code_cleanup/codemod/fix_hardcoded_locators.js +247 -0
  9. package/bin/client/code_cleanup/utils.js +16 -7
  10. package/bin/client/code_gen/code_inversion.js +125 -1
  11. package/bin/client/code_gen/duplication_analysis.js +2 -1
  12. package/bin/client/code_gen/function_signature.js +8 -0
  13. package/bin/client/code_gen/index.js +4 -0
  14. package/bin/client/code_gen/page_reflection.js +90 -9
  15. package/bin/client/code_gen/playwright_codeget.js +173 -77
  16. package/bin/client/codemod/find_harcoded_locators.js +173 -0
  17. package/bin/client/codemod/fix_hardcoded_locators.js +247 -0
  18. package/bin/client/codemod/index.js +8 -0
  19. package/bin/client/codemod/locators_array/find_misstructured_elements.js +148 -0
  20. package/bin/client/codemod/locators_array/fix_misstructured_elements.js +144 -0
  21. package/bin/client/codemod/locators_array/index.js +114 -0
  22. package/bin/client/codemod/types.js +1 -0
  23. package/bin/client/cucumber/feature.js +4 -17
  24. package/bin/client/cucumber/steps_definitions.js +17 -12
  25. package/bin/client/recorderv3/bvt_init.js +310 -0
  26. package/bin/client/recorderv3/bvt_recorder.js +1560 -1183
  27. package/bin/client/recorderv3/constants.js +45 -0
  28. package/bin/client/recorderv3/implemented_steps.js +2 -0
  29. package/bin/client/recorderv3/index.js +3 -293
  30. package/bin/client/recorderv3/mixpanel.js +39 -0
  31. package/bin/client/recorderv3/services.js +839 -142
  32. package/bin/client/recorderv3/step_runner.js +36 -7
  33. package/bin/client/recorderv3/step_utils.js +316 -98
  34. package/bin/client/recorderv3/update_feature.js +85 -37
  35. package/bin/client/recorderv3/utils.js +80 -0
  36. package/bin/client/recorderv3/wbr_entry.js +61 -0
  37. package/bin/client/recording.js +1 -0
  38. package/bin/client/types/locators.js +2 -0
  39. package/bin/client/upload-service.js +2 -0
  40. package/bin/client/utils/app_dir.js +21 -0
  41. package/bin/client/utils/socket_logger.js +100 -125
  42. package/bin/index.js +5 -0
  43. package/package.json +21 -6
  44. package/bin/client/recorderv3/app_dir.js +0 -23
  45. package/bin/client/recorderv3/network.js +0 -299
  46. package/bin/client/recorderv3/scriptTest.js +0 -5
  47. package/bin/client/recorderv3/ws_server.js +0 -72
@@ -1,160 +1,173 @@
1
1
  // define the jsdoc type for the input
2
2
  import { closeContext, initContext, _getDataFile, resetTestData } from "automation_model";
3
- import { existsSync, readdirSync, readFileSync, rmSync } from "fs";
4
- import path from "path";
5
- import url from "url";
3
+ import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
4
+ import path from "node:path";
5
+ import url from "node:url";
6
6
  import { getImplementedSteps, parseRouteFiles } from "./implemented_steps.js";
7
- import { NamesService } from "./services.js";
7
+ import { NamesService, PublishService } from "./services.js";
8
8
  import { BVTStepRunner } from "./step_runner.js";
9
- import { readFile, writeFile } from "fs/promises";
10
- import { updateStepDefinitions, loadStepDefinitions, getCommandsForImplementedStep } from "./step_utils.js";
11
- import { updateFeatureFile } from "./update_feature.js";
9
+ import { readFile, rm, writeFile } from "node:fs/promises";
10
+ import { loadStepDefinitions, getCommandsForImplementedStep, _toRecordingStep, getCucumberStep, getCodePage, toMethodName, } from "./step_utils.js";
12
11
  import { parseStepTextParameters } from "../cucumber/utils.js";
13
12
  import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
14
13
  import chokidar from "chokidar";
15
14
  import { unEscapeNonPrintables } from "../cucumber/utils.js";
16
- import { findAvailablePort } from "../utils/index.js";
17
- import socketLogger from "../utils/socket_logger.js";
15
+ import { findAvailablePort, getRunsServiceBaseURL } from "../utils/index.js";
16
+ import socketLogger, { getErrorMessage } from "../utils/socket_logger.js";
18
17
  import { tmpdir } from "os";
18
+ import { chromium } from "playwright-core";
19
+ import { axiosClient } from "../utils/axiosClient.js";
20
+ import { _generateCodeFromCommand } from "../code_gen/playwright_codeget.js";
21
+ import { Recording } from "../recording.js";
22
+ import { MIXPANEL_EVENTS, mixpanelTrackEvent } from "./mixpanel.js";
19
23
  const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
20
-
21
24
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
22
25
  export function getInitScript(config, options) {
23
- const preScript = `
26
+ const preScript = `
24
27
  window.__bvt_Recorder_config = ${JSON.stringify(config ?? null)};
25
28
  window.__PW_options = ${JSON.stringify(options ?? null)};
26
29
  `;
27
- const recorderScript = readFileSync(
28
- path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"),
29
- "utf8"
30
- );
31
- return preScript + recorderScript;
30
+ const recorderScript = readFileSync(path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"), "utf8");
31
+ return preScript + recorderScript;
32
32
  }
33
-
34
33
  async function evaluate(frame, script) {
35
- if (frame.isDetached()) return;
36
- const url = frame.url();
37
- if (url === "" || url === "about:blank") return;
38
- await frame.evaluate(script);
39
- for (const childFrame of frame.childFrames()) {
40
- await evaluate(childFrame, script);
41
- }
34
+ if (frame.isDetached())
35
+ return;
36
+ const url = frame.url();
37
+ if (url === "" || url === "about:blank")
38
+ return;
39
+ await frame.evaluate(script);
40
+ for (const childFrame of frame.childFrames()) {
41
+ await evaluate(childFrame, script);
42
+ }
42
43
  }
43
-
44
- async function findNestedFrameSelector(frame, obj) {
45
- console.log("Testing new version");
46
- try {
47
- const parent = frame.parentFrame();
48
- if (!parent) return { children: obj };
49
- const frameElement = await frame.frameElement();
50
- if (!frameElement) return;
51
- const selectors = await parent.evaluate((element) => {
52
- return window.__bvt_Recorder.locatorGenerator.getElementLocators(element, { excludeText: true }).locators;
53
- }, frameElement);
54
- return findNestedFrameSelector(parent, { children: obj, selectors });
55
- } catch (e) {
56
- socketLogger.error(`Error in findNestedFrameSelector: ${e}`);
57
- console.error(e);
58
- }
44
+ async function findNestedFrameSelector(frame, obj = {}) {
45
+ try {
46
+ const parent = frame.parentFrame();
47
+ if (!parent)
48
+ return { children: obj };
49
+ const frameElement = await frame.frameElement();
50
+ if (!frameElement)
51
+ return;
52
+ const selectors = await frameElement.evaluate((element) => {
53
+ const recorder = window.__bvt_Recorder;
54
+ return recorder.locatorGenerator.getElementLocators(element, { excludeText: true }).locators;
55
+ });
56
+ return findNestedFrameSelector(parent, { children: obj, selectors });
57
+ }
58
+ catch (e) {
59
+ socketLogger.error(`Error in script evaluation: ${getErrorMessage(e)}`, undefined, "findNestedFrameSelector");
60
+ }
59
61
  }
60
62
  const transformFillAction = (action, el) => {
61
- if (el.tagName.toLowerCase() === "input") {
62
- switch (el.type) {
63
- case "date":
64
- case "datetime-local":
65
- case "month":
66
- case "time":
67
- case "week":
68
- case "range":
69
- case "color":
70
- return {
71
- type: "set_input",
72
- value: action.text,
73
- };
63
+ if (el.tagName.toLowerCase() === "input") {
64
+ switch (el.type) {
65
+ case "date":
66
+ case "datetime-local":
67
+ case "month":
68
+ case "time":
69
+ case "week":
70
+ case "range":
71
+ case "color":
72
+ return {
73
+ type: "set_input",
74
+ value: action.text,
75
+ };
76
+ }
74
77
  }
75
- }
76
- return {
77
- type: "fill_element",
78
- value: action.text,
79
- };
80
- };
81
- const transformClickAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode, isSnapshot) => {
82
- if (isInHoverMode) {
83
- return {
84
- type: "hover_element",
85
- };
86
- }
87
- if (isVerify) {
88
78
  return {
89
- type: "verify_page_contains_text",
90
- value: el.value ?? el.text,
91
- };
92
- }
93
- if (isPopupCloseClick) {
94
- return {
95
- type: "popup_close",
79
+ type: "fill_element",
80
+ value: action.text,
96
81
  };
97
- }
98
- if (isSnapshot) {
82
+ };
83
+ const transformClickAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode, isSnapshot) => {
84
+ if (isInHoverMode) {
85
+ return {
86
+ type: "hover_element",
87
+ };
88
+ }
89
+ if (isVerify) {
90
+ return {
91
+ type: "verify_page_contains_text",
92
+ value: el.value ?? el.text,
93
+ };
94
+ }
95
+ if (isPopupCloseClick) {
96
+ return {
97
+ type: "popup_close",
98
+ };
99
+ }
100
+ if (isSnapshot) {
101
+ return {
102
+ type: "snapshot_element",
103
+ value: action.value,
104
+ };
105
+ }
99
106
  return {
100
- type: "snapshot_element",
101
- value: action.value,
107
+ type: "click_element",
102
108
  };
103
- }
104
- return {
105
- type: "click_element",
106
- };
107
109
  };
108
110
  const transformAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode, isSnapshot) => {
109
- switch (action.name) {
110
- case "click":
111
- return transformClickAction(action, el, isVerify, isPopupCloseClick, isInHoverMode, isSnapshot);
112
- case "fill": {
113
- return transformFillAction(action, el);
114
- }
115
- case "select": {
116
- return {
117
- type: "select_combobox",
118
- value: action.options[0],
119
- };
120
- }
121
-
122
- case "check": {
123
- return {
124
- type: "check_element",
125
- check: true,
126
- };
127
- }
128
- case "uncheck": {
129
- return {
130
- type: "check_element",
131
- check: false,
132
- };
133
- }
134
- case "assertText": {
135
- return {
136
- type: "verify_page_contains_text",
137
- value: action.text,
138
- };
139
- }
140
- case "press": {
141
- return {
142
- type: "press_key",
143
- value: action.key,
144
- };
145
- }
146
- case "setInputFiles": {
147
- return {
148
- type: "set_input_files",
149
- files: action.files,
150
- };
151
- }
152
- default: {
153
- socketLogger.error(`Action not supported: ${action.name}`);
154
- console.log("action not supported", action);
155
- throw new Error("action not supported");
156
- }
157
- }
111
+ switch (action.name) {
112
+ case "click":
113
+ return transformClickAction(action, el, isVerify, isPopupCloseClick, isInHoverMode, isSnapshot);
114
+ case "fill": {
115
+ return transformFillAction(action, el);
116
+ }
117
+ case "select": {
118
+ return {
119
+ type: "select_combobox",
120
+ value: action.options?.[0] ?? "",
121
+ };
122
+ }
123
+ case "check": {
124
+ return {
125
+ type: "check_element",
126
+ check: true,
127
+ };
128
+ }
129
+ case "uncheck": {
130
+ return {
131
+ type: "check_element",
132
+ check: false,
133
+ };
134
+ }
135
+ case "assertText": {
136
+ return {
137
+ type: "verify_page_contains_text",
138
+ value: action.text,
139
+ };
140
+ }
141
+ case "press": {
142
+ return {
143
+ type: "press_key",
144
+ value: action.key,
145
+ };
146
+ }
147
+ case "setInputFiles": {
148
+ return {
149
+ type: "set_input_files",
150
+ files: action.files,
151
+ };
152
+ }
153
+ default: {
154
+ socketLogger.error(`Action not supported: ${action.name}`);
155
+ console.log("action not supported", action);
156
+ throw new Error("action not supported");
157
+ }
158
+ }
159
+ };
160
+ const diffPaths = (currentPath, newPath) => {
161
+ const currentDomain = new URL(currentPath).hostname;
162
+ const newDomain = new URL(newPath).hostname;
163
+ if (currentDomain !== newDomain) {
164
+ return true;
165
+ }
166
+ else {
167
+ const currentRoute = new URL(currentPath).pathname;
168
+ const newRoute = new URL(newPath).pathname;
169
+ return currentRoute !== newRoute;
170
+ }
158
171
  };
159
172
  /**
160
173
  * @typedef {Object} BVTRecorderInput
@@ -165,1074 +178,1438 @@ const transformAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode,
165
178
  * @property {Object} logger
166
179
  */
167
180
  export class BVTRecorder {
168
- #currentURL = "";
169
- #activeFrame = null;
170
- #mode = "noop";
171
- #previousMode = "noop";
172
- #remoteDebuggerPort = null;
173
- /**
174
- *
175
- * @param {BVTRecorderInput} initialState
176
- */
177
- constructor(initialState) {
178
- Object.assign(this, initialState);
179
- this.screenshotMap = new Map();
180
- this.snapshotMap = new Map();
181
- this.scenariosStepsMap = new Map();
182
- this.namesService = new NamesService({
183
- screenshotMap: this.screenshotMap,
184
- TOKEN: this.TOKEN,
185
- projectDir: this.projectDir,
186
- logger: this.logger,
187
- });
188
- this.pageSet = new Set();
189
- this.lastKnownUrlPath = "";
190
- this.world = { attach: () => {} };
191
- this.shouldTakeScreenshot = true;
192
- this.watcher = null;
193
- this.networkEventsFolder = path.join(tmpdir(), "blinq_network_events");
194
- if (existsSync(this.networkEventsFolder)) {
195
- rmSync(this.networkEventsFolder, { recursive: true, force: true });
196
- }
197
- }
198
- events = {
199
- onFrameNavigate: "BVTRecorder.onFrameNavigate",
200
- onPageClose: "BVTRecorder.onPageClose",
201
- onBrowserClose: "BVTRecorder.onBrowserClose",
202
- onNewCommand: "BVTRecorder.command.new",
203
- onCommandDetails: "BVTRecorder.onCommandDetails",
204
- onStepDetails: "BVTRecorder.onStepDetails",
205
- getTestData: "BVTRecorder.getTestData",
206
- onGoto: "BVTRecorder.onGoto",
207
- cmdExecutionStart: "BVTRecorder.cmdExecutionStart",
208
- cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
209
- cmdExecutionError: "BVTRecorder.cmdExecutionError",
210
- interceptResults: "BVTRecorder.interceptResults",
211
- };
212
- bindings = {
213
- __bvt_recordCommand: async ({ frame, page, context }, event) => {
214
- this.#activeFrame = frame;
215
- const nestFrmLoc = await findNestedFrameSelector(frame);
216
- this.logger.info(`Time taken for action: ${event.statistics.time}`);
217
- await this.onAction({ ...event, nestFrmLoc });
218
- },
219
- __bvt_getMode: async () => {
220
- return this.#mode;
221
- },
222
- __bvt_setMode: async (src, mode) => {
223
- await this.setMode(mode);
224
- },
225
- __bvt_revertMode: async () => {
226
- await this.revertMode();
227
- },
228
- __bvt_recordPageClose: async ({ page }) => {
229
- this.pageSet.delete(page);
230
- },
231
- __bvt_closePopups: async () => {
232
- await this.onClosePopup();
233
- },
234
- __bvt_log: async (src, message) => {
235
- this.logger.info(`Inside Browser: ${message}`);
236
- },
237
- __bvt_getObject: (_src, obj) => {
238
- this.processObject(obj);
239
- },
240
- };
241
-
242
- getSnapshot = async (attr) => {
243
- const selector = `[__bvt_snapshot="${attr}"]`;
244
- const locator = await this.web.page.locator(selector);
245
- const snapshot = await locator.ariaSnapshot();
246
- return snapshot;
247
- };
248
-
249
- processObject = async ({ type, action, value }) => {
250
- switch (type) {
251
- case "snapshot-element": {
252
- if (action === "get-template") {
253
- return true;
254
- }
255
- break;
256
- }
257
- default: {
258
- console.log("Unknown object type", type);
259
- break;
260
- }
261
- }
262
- };
263
-
264
- getPWScript() {
265
- const pwFolder = path.join(__dirname, "..", "..", "assets", "preload", "pw_utils");
266
- const result = [];
267
- for (const script of readdirSync(pwFolder)) {
268
- const path = path.join(pwFolder, script);
269
- const content = readFileSync(path, "utf8");
270
- result.push(content);
271
- }
272
- return result;
273
- }
274
- getRecorderScripts() {
275
- const recorderFolder = path.join(__dirname, "..", "..", "assets", "preload", "recorder");
276
- const result = [];
277
- for (const script of readdirSync(recorderFolder)) {
278
- const path = path.join(recorderFolder, script);
279
- const content = readFileSync(path, "utf8");
280
- result.push(content);
281
- }
282
- return result;
283
- }
284
- getInitScripts(config) {
285
- return getInitScript(config, {
286
- sdkLanguage: "javascript",
287
- testIdAttributeName: "blinq-test-id",
288
- stableRafCount: 0,
289
- browserName: this.browser?.browserType().name(),
290
- inputFileRoleTextbox: false,
291
- customEngines: [],
292
- isUnderTest: true,
293
- });
294
- }
295
-
296
- async _initBrowser({ url }) {
297
- this.#remoteDebuggerPort = await findAvailablePort();
298
- process.env.CDP_LISTEN_PORT = this.#remoteDebuggerPort;
299
-
300
- // this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
301
- this.world = { attach: () => {} };
302
-
303
- const ai_config_file = path.join(this.projectDir, "ai_config.json");
304
- let ai_config = {};
305
- if (existsSync(ai_config_file)) {
306
- try {
307
- ai_config = JSON.parse(readFileSync(ai_config_file, "utf8"));
308
- } catch (error) {
309
- this.logger.error("Error reading ai_config.json", error);
310
- }
311
- }
312
- this.config = ai_config;
313
- const initScripts = {
314
- // recorderCjs: injectedScriptSource,
315
- scripts: [
316
- this.getInitScripts(ai_config),
317
- `\ndelete Object.getPrototypeOf(navigator).webdriver;${process.env.WINDOW_DEBUGGER ? "window.debug=true;\n" : ""}`,
318
- ],
181
+ #currentURL = "";
182
+ #activeFrame = null;
183
+ #mode = "noop";
184
+ #previousMode = "noop";
185
+ #remoteDebuggerPort = null;
186
+ envName;
187
+ projectDir;
188
+ TOKEN;
189
+ sendEvent;
190
+ logger;
191
+ userId;
192
+ screenshotMap = new Map();
193
+ snapshotMap = new Map();
194
+ scenariosStepsMap = new Map();
195
+ namesService;
196
+ workspaceService;
197
+ pageSet = new Set();
198
+ lastKnownUrlPath = "";
199
+ world = {
200
+ attach: async () => { },
201
+ parameters: {},
202
+ log: (message) => {
203
+ socketLogger.log.call(socketLogger, "info", message, undefined, "Cucumber.JS World");
204
+ },
319
205
  };
320
-
321
- const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName);
322
- this.bvtContext = bvtContext;
323
- this.stepRunner = new BVTStepRunner({
324
- projectDir: this.projectDir,
325
- sendExecutionStatus: (data) => {
326
- if (data && data.type) {
327
- switch (data.type) {
328
- case "cmdExecutionStart":
329
- this.sendEvent(this.events.cmdExecutionStart, data);
330
- break;
331
- case "cmdExecutionSuccess":
332
- this.sendEvent(this.events.cmdExecutionSuccess, data);
333
- break;
334
- case "cmdExecutionError":
335
- this.sendEvent(this.events.cmdExecutionError, data);
336
- break;
337
- case "interceptResults":
338
- this.sendEvent(this.events.interceptResults, data);
339
- break;
340
- default:
341
- break;
342
- }
343
- }
344
- },
345
- bvtContext: this.bvtContext,
346
- });
347
- const context = bvtContext.playContext;
348
- this.context = context;
349
- this.web = bvtContext.stable || bvtContext.web;
350
- this.web.tryAllStrategies = true;
351
- this.page = bvtContext.page;
352
- this.pageSet.add(this.page);
353
- this.lastKnownUrlPath = this._updateUrlPath();
354
- const browser = await this.context.browser();
355
- this.browser = browser;
356
-
357
- // add bindings
358
- for (const [name, handler] of Object.entries(this.bindings)) {
359
- await this.context.exposeBinding(name, handler);
360
- }
361
- this._watchTestData();
362
- this.web.onRestoreSaveState = (url) => {
363
- this._initBrowser({ url });
206
+ shouldTakeScreenshot = true;
207
+ watcher = null;
208
+ networkEventsFolder;
209
+ tempProjectFolder;
210
+ tempSnapshotsFolder;
211
+ config = {};
212
+ bvtContext;
213
+ stepRunner;
214
+ context;
215
+ web;
216
+ page;
217
+ browser = null;
218
+ backgroundBrowser;
219
+ backgroundContext;
220
+ timerId = null;
221
+ previousIndex = null;
222
+ previousHistoryLength = null;
223
+ previousEntries = null;
224
+ previousUrl = null;
225
+ scenarioDoc;
226
+ isVerify = false;
227
+ /**
228
+ * @param initialState Initial recorder state and dependencies
229
+ */
230
+ constructor(initialState) {
231
+ this.envName = initialState.envName;
232
+ this.projectDir = initialState.projectDir;
233
+ this.TOKEN = initialState.TOKEN;
234
+ this.sendEvent = initialState.sendEvent;
235
+ this.logger = initialState.logger;
236
+ this.userId = initialState.userId;
237
+ this.namesService = new NamesService({
238
+ screenshotMap: this.screenshotMap,
239
+ TOKEN: this.TOKEN,
240
+ projectDir: this.projectDir,
241
+ logger: this.logger,
242
+ });
243
+ this.workspaceService = new PublishService(this.TOKEN);
244
+ this.networkEventsFolder = path.join(tmpdir(), "blinq_network_events");
245
+ this.tempProjectFolder = `${tmpdir()}/bvt_temp_project_${Math.floor(Math.random() * 1000000)}`;
246
+ this.tempSnapshotsFolder = path.join(this.tempProjectFolder, "data/snapshots");
247
+ if (existsSync(this.networkEventsFolder)) {
248
+ rmSync(this.networkEventsFolder, { recursive: true, force: true });
249
+ }
250
+ }
251
+ events = {
252
+ onFrameNavigate: "BVTRecorder.onFrameNavigate",
253
+ onPageClose: "BVTRecorder.onPageClose",
254
+ onBrowserClose: "BVTRecorder.onBrowserClose",
255
+ onNewCommand: "BVTRecorder.command.new",
256
+ onCommandDetails: "BVTRecorder.onCommandDetails",
257
+ onStepDetails: "BVTRecorder.onStepDetails",
258
+ getTestData: "BVTRecorder.getTestData",
259
+ onGoto: "BVTRecorder.onGoto",
260
+ cmdExecutionStart: "BVTRecorder.cmdExecutionStart",
261
+ cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
262
+ cmdExecutionError: "BVTRecorder.cmdExecutionError",
263
+ interceptResults: "BVTRecorder.interceptResults",
264
+ onDebugURLChange: "BVTRecorder.onDebugURLChange",
265
+ updateCommand: "BVTRecorder.updateCommand",
364
266
  };
365
- }
366
- async onClosePopup() {
367
- // console.log("close popups");
368
- await this.bvtContext.web.closeUnexpectedPopups();
369
- }
370
- async evaluateInAllFrames(context, script) {
371
- // retry 3 times
372
- for (let i = 0; i < 3; i++) {
373
- try {
374
- for (const page of context.pages()) {
375
- await evaluate(page.mainFrame(), script);
267
+ bindings = {
268
+ __bvt_recordCommand: async ({ frame, page, context }, event) => {
269
+ this.#activeFrame = frame;
270
+ const nestFrmLoc = await findNestedFrameSelector(frame);
271
+ if (event.statistics?.time !== undefined) {
272
+ this.logger.info(`Time taken for action: ${event.statistics.time}`);
273
+ }
274
+ await this.onAction({ ...event, nestFrmLoc });
275
+ },
276
+ __bvt_getMode: async () => {
277
+ return this.#mode;
278
+ },
279
+ __bvt_setMode: async (_src, mode) => {
280
+ await this.setMode(mode);
281
+ },
282
+ __bvt_revertMode: async () => {
283
+ await this.revertMode();
284
+ },
285
+ __bvt_recordPageClose: async ({ page }) => {
286
+ this.pageSet.delete(page);
287
+ },
288
+ __bvt_closePopups: async () => {
289
+ await this.onClosePopup();
290
+ },
291
+ __bvt_log: async (_src, message) => {
292
+ this.logger.info(`Inside Browser: ${message}`);
293
+ },
294
+ __bvt_getObject: (_src, obj) => {
295
+ this.processObject(obj);
296
+ },
297
+ };
298
+ getSnapshot = async (attr) => {
299
+ const selector = `[__bvt_snapshot="${attr}"]`;
300
+ const locator = await this.web.page.locator(selector);
301
+ const snapshot = await locator.ariaSnapshot();
302
+ return snapshot;
303
+ };
304
+ processObject = async ({ type, action, value }) => {
305
+ switch (type) {
306
+ case "snapshot-element": {
307
+ if (action === "get-template") {
308
+ return true;
309
+ }
310
+ break;
311
+ }
312
+ default: {
313
+ console.log("Unknown object type", type);
314
+ break;
315
+ }
376
316
  }
377
- return;
378
- } catch (error) {
379
- // console.error("Error evaluting in context:", error);
380
- this.logger.error("Error evaluating in context:", error);
381
- }
382
- }
383
- }
384
-
385
- getMode() {
386
- // console.log("getMode", this.#mode);
387
- this.logger.info("Current mode:", this.#mode);
388
- return this.#mode;
389
- }
390
-
391
- async setMode(mode) {
392
- await this.evaluateInAllFrames(this.context, `window.__bvt_Recorder.mode = "${mode}";`);
393
- this.#previousMode = this.#mode;
394
- this.#mode = mode;
395
- }
396
- async revertMode() {
397
- await this.setMode(this.#previousMode);
398
- }
399
-
400
- async _openTab({ url }) {
401
- // add listeners for new pages
402
- this._addPagelisteners(this.context);
403
-
404
- await this.page.goto(url, {
405
- waitUntil: "domcontentloaded",
406
- });
407
- // add listener for frame navigation on current tab
408
- this._addFrameNavigateListener(this.page);
409
-
410
- // eval init script on current tab
411
- // await this._initPage(this.page);
412
- this.#currentURL = new URL(url).pathname;
413
-
414
- await this.page.dispatchEvent("html", "scroll");
415
- await delay(1000);
416
- }
417
- _addFrameNavigateListener(page) {
418
- page.on("close", () => {
419
- try {
420
- if (!this.pageSet.has(page)) return;
421
- // console.log(this.context.pages().length);
422
- if (this.context.pages().length > 0) {
423
- this.sendEvent(this.events.onPageClose);
424
- } else {
425
- // closed all tabs
426
- this.sendEvent(this.events.onBrowserClose);
427
- }
428
- } catch (error) {
429
- this.logger.error("Error in page close event");
430
- this.logger.error(error);
431
- console.error("Error in page close event");
432
- console.error(error);
433
- }
434
- });
435
-
436
- page.on("framenavigated", async (frame) => {
437
- try {
438
- if (frame !== page.mainFrame()) return;
439
- this.handlePageTransition();
440
- } catch (error) {
441
- this.logger.error("Error in handlePageTransition event");
442
- this.logger.error(error);
443
- console.error("Error in handlePageTransition event");
444
- console.error(error);
445
- }
446
- try {
447
- if (frame !== this.#activeFrame) return;
448
-
449
- // hack to sync the action event with the frame navigation
450
- await this.storeScreenshot({
451
- element: { inputID: "frame" },
317
+ };
318
+ getPWScript() {
319
+ const pwFolder = path.join(__dirname, "..", "..", "assets", "preload", "pw_utils");
320
+ const result = [];
321
+ for (const script of readdirSync(pwFolder)) {
322
+ const scriptPath = path.join(pwFolder, script);
323
+ const content = readFileSync(scriptPath, "utf8");
324
+ result.push(content);
325
+ }
326
+ return result;
327
+ }
328
+ getRecorderScripts() {
329
+ const recorderFolder = path.join(__dirname, "..", "..", "assets", "preload", "recorder");
330
+ const result = [];
331
+ for (const script of readdirSync(recorderFolder)) {
332
+ const scriptPath = path.join(recorderFolder, script);
333
+ const content = readFileSync(scriptPath, "utf8");
334
+ result.push(content);
335
+ }
336
+ return result;
337
+ }
338
+ getInitScripts(config) {
339
+ return getInitScript(config, {
340
+ sdkLanguage: "javascript",
341
+ testIdAttributeName: "blinq-test-id",
342
+ stableRafCount: 0,
343
+ browserName: this.browser?.browserType().name(),
344
+ inputFileRoleTextbox: false,
345
+ customEngines: [],
346
+ isUnderTest: true,
452
347
  });
453
-
454
- const newPath = new URL(frame.url()).pathname;
455
- const newTitle = await frame.title();
456
- if (newPath !== this.#currentURL) {
457
- this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
458
- this.#currentURL = newPath;
459
- }
460
- // await this._setRecordingMode(frame);
461
- // await this._initPage(page);
462
- } catch (error) {
463
- this.logger.error("Error in frame navigate event");
464
- this.logger.error(error);
465
- console.error("Error in frame navigate event");
466
- // console.error(error);
467
- }
468
- });
469
- }
470
-
471
- hasHistoryReplacementAtIndex(previousEntries, currentEntries, index) {
472
- if (!previousEntries || !currentEntries) return false;
473
- if (index >= previousEntries.length || index >= currentEntries.length) return false;
474
-
475
- const prevEntry = previousEntries[index];
476
- // console.log("prevEntry", prevEntry);
477
- const currEntry = currentEntries[index];
478
- // console.log("currEntry", currEntry);
479
-
480
- // Check if the entry at this index has been replaced
481
- return prevEntry.id !== currEntry.id;
482
- }
483
-
484
- // Even simpler approach for your specific case
485
- analyzeTransitionType(entries, currentIndex, currentEntry) {
486
- // console.log("Analyzing transition type");
487
- // console.log("===========================");
488
- // console.log("Current Index:", currentIndex);
489
- // console.log("Current Entry:", currentEntry);
490
- // console.log("Current Entries:", entries);
491
- // console.log("Current entries length:", entries.length);
492
- // console.log("===========================");
493
- // console.log("Previous Index:", this.previousIndex);
494
- // // console.log("Previous Entry:", this.previousEntries[this.previousIndex]);
495
- // console.log("Previous Entries:", this.previousEntries);
496
- // console.log("Previous entries length:", this.previousHistoryLength);
497
-
498
- if (this.previousIndex === null || this.previousHistoryLength === null || !this.previousEntries) {
499
- return {
500
- action: "initial",
501
- };
502
- }
503
-
504
- const indexDiff = currentIndex - this.previousIndex;
505
- const lengthDiff = entries.length - this.previousHistoryLength;
506
-
507
- // Backward navigation
508
- if (indexDiff < 0) {
509
- return { action: "back" };
510
- }
511
-
512
- // Forward navigation
513
- if (indexDiff > 0 && lengthDiff === 0) {
514
- // Check if the entry at current index is the same as before
515
- const entryReplaced = this.hasHistoryReplacementAtIndex(this.previousEntries, entries, currentIndex);
516
-
517
- if (entryReplaced) {
518
- return { action: "navigate" }; // New navigation that replaced forward history
519
- } else {
520
- return { action: "forward" }; // True forward navigation
521
- }
522
- }
523
-
524
- // New navigation (history grew)
525
- if (lengthDiff > 0) {
526
- return { action: "navigate" };
527
- }
528
-
529
- // Same position, same length
530
- if (lengthDiff <= 0) {
531
- const entryReplaced = this.hasHistoryReplacementAtIndex(this.previousEntries, entries, currentIndex);
532
-
533
- return entryReplaced ? { action: "navigate" } : { action: "reload" };
534
- }
535
-
536
- return { action: "unknown" };
537
- }
538
-
539
- async getCurrentTransition() {
540
- if (this?.web?.browser?._name !== "chromium") {
541
- return;
542
- }
543
- const client = await this.context.newCDPSession(this.web.page);
544
-
545
- try {
546
- const result = await client.send("Page.getNavigationHistory");
547
- const entries = result.entries;
548
- const currentIndex = result.currentIndex;
549
-
550
- const currentEntry = entries[currentIndex];
551
- const transitionInfo = this.analyzeTransitionType(entries, currentIndex, currentEntry);
552
- this.previousIndex = currentIndex;
553
- this.previousHistoryLength = entries.length;
554
- this.previousUrl = currentEntry.url;
555
- this.previousEntries = [...entries]; // Store a copy of current entries
556
-
557
- return {
558
- currentEntry,
559
- navigationAction: transitionInfo.action,
560
- };
561
- } catch (error) {
562
- this.logger.error("Error in getCurrentTransition event");
563
- this.logger.error(error);
564
- console.error("Error in getTransistionType event", error);
565
- } finally {
566
- await client.detach();
567
- }
568
- }
569
- userInitiatedTransitionTypes = ["typed", "address_bar"];
570
- async handlePageTransition() {
571
- const transition = await this.getCurrentTransition();
572
- if (!transition) return;
573
-
574
- const { currentEntry, navigationAction } = transition;
575
-
576
- switch (navigationAction) {
577
- case "initial":
578
- // console.log("Initial navigation, no action taken");
579
- return;
580
- case "navigate":
581
- // console.log("transitionType", transition.transitionType);
582
- // console.log("sending onGoto event", { url: currentEntry.url,
583
- // type: "navigate", });
584
- if (this.userInitiatedTransitionTypes.includes(currentEntry.transitionType)) {
585
- const env = JSON.parse(readFileSync(this.envName), "utf8");
586
- const baseUrl = env.baseUrl;
587
- let url = currentEntry.userTypedURL;
588
- if (baseUrl && url.startsWith(baseUrl)) {
589
- url = url.replace(baseUrl, "{{env.baseUrl}}");
590
- }
591
- // console.log("User initiated transition");
592
- this.sendEvent(this.events.onGoto, { url, type: "navigate" });
348
+ }
349
+ async _initBrowser({ url }) {
350
+ if (process.env.CDP_LISTEN_PORT === undefined) {
351
+ this.#remoteDebuggerPort = await findAvailablePort();
352
+ process.env.CDP_LISTEN_PORT = String(this.#remoteDebuggerPort);
593
353
  }
594
- return;
595
- case "back":
596
- // console.log("User navigated back");
597
- // console.log("sending onGoto event", {
598
- // type: "back",
599
- // });
600
- this.sendEvent(this.events.onGoto, { type: "back" });
601
- return;
602
- case "forward":
603
- // console.log("User navigated forward"); console.log("sending onGoto event", { type: "forward", });
604
- this.sendEvent(this.events.onGoto, { type: "forward" });
605
- return;
606
- default:
607
- this.sendEvent(this.events.onGoto, { type: "unknown" });
608
- return;
354
+ else {
355
+ this.#remoteDebuggerPort = Number(process.env.CDP_LISTEN_PORT);
356
+ }
357
+ // this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
358
+ const ai_config_file = path.join(this.projectDir, "ai_config.json");
359
+ let ai_config = {};
360
+ if (existsSync(ai_config_file)) {
361
+ try {
362
+ ai_config = JSON.parse(readFileSync(ai_config_file, "utf8"));
363
+ }
364
+ catch (error) {
365
+ this.logger.error("Error reading ai_config.json", { error });
366
+ }
367
+ }
368
+ this.config = ai_config;
369
+ const initScripts = {
370
+ recorderCjs: null,
371
+ scripts: [
372
+ this.getInitScripts(ai_config),
373
+ `\ndelete Object.getPrototypeOf(navigator).webdriver;${process.env.WINDOW_DEBUGGER ? "window.debug=true;\n" : ""}`,
374
+ ],
375
+ };
376
+ const scenario = { pickle: this.scenarioDoc };
377
+ const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName, scenario);
378
+ this.bvtContext = bvtContext;
379
+ this.stepRunner = new BVTStepRunner({
380
+ projectDir: this.projectDir,
381
+ sendExecutionStatus: (data) => {
382
+ if (data && data.type) {
383
+ switch (data.type) {
384
+ case "cmdExecutionStart":
385
+ this.sendEvent(this.events.cmdExecutionStart, data);
386
+ break;
387
+ case "cmdExecutionSuccess":
388
+ this.sendEvent(this.events.cmdExecutionSuccess, data);
389
+ break;
390
+ case "cmdExecutionError":
391
+ this.sendEvent(this.events.cmdExecutionError, data);
392
+ break;
393
+ case "interceptResults":
394
+ this.sendEvent(this.events.interceptResults, data);
395
+ break;
396
+ default:
397
+ break;
398
+ }
399
+ }
400
+ },
401
+ bvtContext: this.bvtContext,
402
+ });
403
+ this.context = bvtContext.playContext;
404
+ this.web = bvtContext.stable || bvtContext.web;
405
+ this.web.tryAllStrategies = true;
406
+ this.page = bvtContext.page;
407
+ this.pageSet.add(this.page);
408
+ this.lastKnownUrlPath = this._updateUrlPath();
409
+ const browser = await this.context.browser();
410
+ this.browser = browser;
411
+ // add bindings
412
+ for (const [name, handler] of Object.entries(this.bindings)) {
413
+ await this.context.exposeBinding(name, handler);
414
+ }
415
+ this._watchTestData();
416
+ this.web.onRestoreSaveState = async (url) => {
417
+ await this._initBrowser({ url });
418
+ this._addPagelisteners(this.context);
419
+ this._addFrameNavigateListener(this.page);
420
+ };
421
+ // create a second browser for locator generation
422
+ this.backgroundBrowser = await chromium.launch({
423
+ headless: true,
424
+ });
425
+ this.backgroundContext = await this.backgroundBrowser.newContext({});
426
+ await this.backgroundContext.addInitScript({ content: this.getInitScripts(this.config) });
427
+ await this.backgroundContext.newPage();
609
428
  }
610
- }
611
-
612
- async getCurrentPageTitle() {
613
- const title = await this.page.title();
614
- return title;
615
- }
616
- async getCurrentPageUrl() {
617
- const url = await this.page.url();
618
- return url;
619
- }
620
-
621
- _addPagelisteners(context) {
622
- context.on("page", async (page) => {
623
- try {
624
- if (page.isClosed()) return;
625
- this.pageSet.add(page);
626
-
627
- await page.waitForLoadState("domcontentloaded");
628
-
629
- // add listener for frame navigation on new tab
630
- this._addFrameNavigateListener(page);
631
- } catch (error) {
632
- this.logger.error("Error in page event");
633
- this.logger.error(error);
634
- console.error("Error in page event");
635
- console.error(error);
636
- }
637
- });
638
- }
639
- async openBrowser() {
640
- const env = JSON.parse(readFileSync(this.envName), "utf8");
641
- const url = env.baseUrl;
642
- await this._initBrowser({ url });
643
- await this._openTab({ url });
644
- process.env.TEMP_RUN = true;
645
- }
646
- overlayLocators(event) {
647
- let locatorsResults = [...event.locators];
648
- const cssLocators = event.cssLocators;
649
- for (const cssLocator of cssLocators) {
650
- locatorsResults.push({ mode: "NO_TEXT", css: cssLocator });
651
- }
652
- if (event.digitLocators) {
653
- for (const digitLocator of event.digitLocators) {
654
- digitLocator.mode = "IGNORE_DIGIT";
655
- locatorsResults.push(digitLocator);
656
- }
657
- }
658
- if (event.contextLocator) {
659
- locatorsResults.push({
660
- mode: "CONTEXT",
661
- text: event.contextLocator.texts[0],
662
- css: event.contextLocator.css,
663
- climb: event.contextLocator.climbCount,
664
- });
665
- }
666
- return locatorsResults;
667
- }
668
- async getScreenShot() {
669
- const client = await this.context.newCDPSession(this.web.page);
670
- try {
671
- // Using CDP to capture the screenshot
672
- const { data } = await client.send("Page.captureScreenshot", { format: "png" });
673
- return data;
674
- } catch (error) {
675
- this.logger.error("Error in taking browser screenshot");
676
- console.error("Error in taking browser screenshot", error);
677
- } finally {
678
- await client.detach();
679
- }
680
- }
681
- async storeScreenshot(event) {
682
- try {
683
- // const spath = path.join(__dirname, "media", `${event.inputID}.png`);
684
- const screenshotURL = await this.getScreenShot();
685
- this.screenshotMap.set(event.element.inputID, screenshotURL);
686
- // writeFileSync(spath, screenshotURL, "base64");
687
- } catch (error) {
688
- console.error("Error in saving screenshot: ", error);
689
- }
690
- }
691
- async onAction(event) {
692
- this._updateUrlPath();
693
- // const locators = this.overlayLocators(event);
694
- const cmdEvent = {
695
- ...event.element,
696
- ...transformAction(
697
- event.action,
698
- event.element,
699
- event.mode === "recordingText" || event.mode === "recordingContext",
700
- event.isPopupCloseClick,
701
- event.mode === "recordingHover",
702
- event.mode === "multiInspecting"
703
- ),
704
- locators: {
705
- locators: event.locators,
706
- iframe_src: !event.frame.isTop ? event.frame.url : undefined,
707
- },
708
- allStrategyLocators: event.allStrategyLocators,
709
- url: event.frame.url,
710
- title: event.frame.title,
711
- extract: {},
712
- lastKnownUrlPath: this.lastKnownUrlPath,
713
- };
714
- if (event.nestFrmLoc?.children) {
715
- cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
429
+ async onClosePopup() {
430
+ // console.log("close popups");
431
+ await this.bvtContext.web?.closeUnexpectedPopups(null, null);
716
432
  }
717
- // this.logger.info({ event });
718
- if (this.shouldTakeScreenshot) {
719
- await this.storeScreenshot(event);
433
+ async evaluateInAllFrames(context, script) {
434
+ // retry 3 times
435
+ for (let i = 0; i < 3; i++) {
436
+ try {
437
+ for (const page of context.pages()) {
438
+ await evaluate(page.mainFrame(), script);
439
+ }
440
+ return;
441
+ }
442
+ catch (error) {
443
+ // console.error("Error evaluting in context:", error);
444
+ this.logger.error("Error evaluating in context", { error });
445
+ }
446
+ }
720
447
  }
721
- this.sendEvent(this.events.onNewCommand, cmdEvent);
722
- this._updateUrlPath();
723
- }
724
- _updateUrlPath() {
725
- try {
726
- let url = this.bvtContext.web.page.url();
727
- if (url === "about:blank") {
728
- return;
729
- } else {
730
- this.lastKnownUrlPath = new URL(url).pathname;
731
- }
732
- } catch (error) {
733
- console.error("Error in getting last known url path", error);
734
- }
735
- }
736
- async closeBrowser() {
737
- delete process.env.TEMP_RUN;
738
- await this.watcher.close().then(() => {});
739
- this.watcher = null;
740
- this.previousIndex = null;
741
- this.previousHistoryLength = null;
742
- this.previousUrl = null;
743
- this.previousEntries = null;
744
-
745
- await closeContext();
746
- this.pageSet.clear();
747
- }
748
- async reOpenBrowser(input) {
749
- if (input && input.envName) {
750
- this.envName = path.join(this.projectDir, "environments", input.envName + ".json");
751
- process.env.BLINQ_ENV = this.envName;
752
- }
753
- await this.closeBrowser();
754
- // logger.log("closed");
755
- await delay(1000);
756
- await this.openBrowser();
757
- // logger.log("opened");
758
- }
759
- async getNumberOfOccurrences({ searchString, regex = false, partial = true, ignoreCase = false, tag = "*" }) {
760
- this.isVerify = false;
761
- //const script = `window.countStringOccurrences(${JSON.stringify(searchString)});`;
762
- if (searchString.length === 0) return -1;
763
- let result = 0;
764
- for (let i = 0; i < 3; i++) {
765
- result = 0;
766
- try {
767
- for (const page of this.context.pages()) {
768
- for (const frame of page.frames()) {
448
+ getMode() {
449
+ // console.log("getMode", this.#mode);
450
+ this.logger.info("Current mode", { mode: this.#mode });
451
+ return this.#mode;
452
+ }
453
+ async setMode(mode) {
454
+ await this.evaluateInAllFrames(this.context, `window.__bvt_Recorder.mode = "${mode}";`);
455
+ this.#previousMode = this.#mode;
456
+ this.#mode = mode;
457
+ }
458
+ async revertMode() {
459
+ await this.setMode(this.#previousMode);
460
+ }
461
+ async _openTab({ url }) {
462
+ // add listeners for new pages
463
+ this._addPagelisteners(this.context);
464
+ await this.page.goto(url, {
465
+ waitUntil: "domcontentloaded",
466
+ timeout: typeof this.config.page_timeout === "number" ? this.config.page_timeout : 60_000,
467
+ });
468
+ // add listener for frame navigation on current tab
469
+ this._addFrameNavigateListener(this.page);
470
+ // eval init script on current tab
471
+ // await this._initPage(this.page);
472
+ this.#currentURL = url;
473
+ await this.page.dispatchEvent("html", "scroll");
474
+ await delay(1000);
475
+ }
476
+ _addFrameNavigateListener(page) {
477
+ page.on("close", () => {
769
478
  try {
770
- //scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params: Params)
771
- const frameResult = await this.web._locateElementByText(
772
- frame,
773
- searchString,
774
- tag,
775
- regex,
776
- partial,
777
- ignoreCase,
778
- {}
779
- );
780
- result += frameResult.elementCount;
781
- } catch (e) {
782
- console.log(e);
479
+ if (!this.pageSet.has(page))
480
+ return;
481
+ // console.log(this.context.pages().length);
482
+ if (this.context.pages().length > 0) {
483
+ this.sendEvent(this.events.onPageClose);
484
+ }
485
+ else {
486
+ // closed all tabs
487
+ this.sendEvent(this.events.onBrowserClose);
488
+ }
489
+ }
490
+ catch (error) {
491
+ this.logger.error("Error in page close event", { error });
492
+ console.error("Error in page close event");
493
+ console.error(error);
783
494
  }
784
- }
495
+ });
496
+ page.on("framenavigated", async (frame) => {
497
+ try {
498
+ if (frame !== page.mainFrame())
499
+ return;
500
+ await this.handlePageTransition();
501
+ }
502
+ catch (error) {
503
+ this.logger.error("Error in handlePageTransition event", { error });
504
+ console.error("Error in handlePageTransition event");
505
+ console.error(error);
506
+ }
507
+ try {
508
+ if (frame !== this.#activeFrame)
509
+ return;
510
+ // hack to sync the action event with the frame navigation
511
+ await this.storeScreenshot({
512
+ element: { inputID: "frame" },
513
+ });
514
+ const newUrl = frame.url();
515
+ const newPath = new URL(newUrl).pathname;
516
+ const newTitle = await frame.title();
517
+ const changed = diffPaths(this.#currentURL, newUrl);
518
+ if (changed) {
519
+ this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
520
+ this.#currentURL = newUrl;
521
+ }
522
+ }
523
+ catch (error) {
524
+ this.logger.error("Error in frame navigate event", { error });
525
+ console.error("Error in frame navigate event");
526
+ // console.error(error);
527
+ }
528
+ });
529
+ }
530
+ hasHistoryReplacementAtIndex(previousEntries, currentEntries, index) {
531
+ if (!previousEntries || !currentEntries)
532
+ return false;
533
+ if (index >= previousEntries.length || index >= currentEntries.length)
534
+ return false;
535
+ const prevEntry = previousEntries[index];
536
+ // console.log("prevEntry", prevEntry);
537
+ const currEntry = currentEntries[index];
538
+ // console.log("currEntry", currEntry);
539
+ // Check if the entry at this index has been replaced
540
+ return prevEntry.id !== currEntry.id;
541
+ }
542
+ // Even simpler approach for your specific case
543
+ analyzeTransitionType(entries, currentIndex, currentEntry) {
544
+ // console.log("Analyzing transition type");
545
+ // console.log("===========================");
546
+ // console.log("Current Index:", currentIndex);
547
+ // console.log("Current Entry:", currentEntry);
548
+ // console.log("Current Entries:", entries);
549
+ // console.log("Current entries length:", entries.length);
550
+ // console.log("===========================");
551
+ // console.log("Previous Index:", this.previousIndex);
552
+ // // console.log("Previous Entry:", this.previousEntries[this.previousIndex]);
553
+ // console.log("Previous Entries:", this.previousEntries);
554
+ // console.log("Previous entries length:", this.previousHistoryLength);
555
+ if (this.previousIndex === null || this.previousHistoryLength === null || !this.previousEntries) {
556
+ return {
557
+ action: "initial",
558
+ };
785
559
  }
786
-
787
- return result;
788
- } catch (e) {
789
- console.log(e);
790
- result = 0;
791
- }
792
- }
793
- }
794
-
795
- async startRecordingInput() {
796
- await this.setMode("recordingInput");
797
- }
798
- async stopRecordingInput() {
799
- await this.setMode("idle");
800
- }
801
- async startRecordingText(isInspectMode) {
802
- if (isInspectMode) {
803
- await this.setMode("inspecting");
804
- } else {
805
- await this.setMode("recordingText");
806
- }
807
- }
808
- async stopRecordingText() {
809
- await this.setMode("idle");
810
- }
811
- async startRecordingContext() {
812
- await this.setMode("recordingContext");
813
- }
814
- async stopRecordingContext() {
815
- await this.setMode("idle");
816
- }
817
-
818
- async abortExecution() {
819
- await this.stepRunner.abortExecution();
820
- }
821
-
822
- async pauseExecution({ cmdId }) {
823
- await this.stepRunner.pauseExecution(cmdId);
824
- }
825
-
826
- async resumeExecution({ cmdId }) {
827
- await this.stepRunner.resumeExecution(cmdId);
828
- }
829
-
830
- async dealyedRevertMode() {
831
- const timerId = setTimeout(async () => {
832
- await this.revertMode();
833
- }, 100);
834
- this.timerId = timerId;
835
- }
836
- async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
837
- const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
838
- const _env = {
839
- TOKEN: this.TOKEN,
840
- TEMP_RUN: true,
841
- REPORT_FOLDER: this.bvtContext.reportFolder,
842
- BLINQ_ENV: this.envName,
843
- DEBUG: "blinq:route",
844
- };
845
-
846
- this.bvtContext.navigate = true;
847
- this.bvtContext.loadedRoutes = null;
848
- if (listenNetwork) {
849
- this.bvtContext.STORE_DETAILED_NETWORK_DATA = true;
850
- } else {
851
- this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
852
- }
853
- for (const [key, value] of Object.entries(_env)) {
854
- process.env[key] = value;
855
- }
856
-
857
- if (this.timerId) {
858
- clearTimeout(this.timerId);
859
- this.timerId = null;
860
- }
861
- await this.setMode("running");
862
-
863
- try {
864
- const { result, info } = await this.stepRunner.runStep(
865
- {
866
- step,
867
- parametersMap,
868
- envPath: this.envName,
869
- tags,
870
- config: this.config,
871
- },
872
- this.bvtContext,
873
- {
874
- skipAfter,
875
- skipBefore,
876
- }
877
- );
878
- await this.revertMode();
879
- return { info };
880
- } catch (error) {
881
- await this.revertMode();
882
- throw error;
883
- } finally {
884
- for (const key of Object.keys(_env)) {
885
- delete process.env[key];
886
- }
887
- this.bvtContext.navigate = false;
888
- }
889
- }
890
- async saveScenario({ scenario, featureName, override, isSingleStep }) {
891
- await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
892
- if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
893
- await this.cleanup({ tags: scenario.tags });
894
- }
895
- async getImplementedSteps() {
896
- const stepsAndScenarios = await getImplementedSteps(this.projectDir);
897
- const implementedSteps = stepsAndScenarios.implementedSteps;
898
- const scenarios = stepsAndScenarios.scenarios;
899
- for (const scenario of scenarios) {
900
- this.scenariosStepsMap.set(scenario.name, scenario.steps);
901
- delete scenario.steps;
560
+ const indexDiff = currentIndex - this.previousIndex;
561
+ const lengthDiff = entries.length - this.previousHistoryLength;
562
+ // Backward navigation
563
+ if (indexDiff < 0) {
564
+ return { action: "back" };
565
+ }
566
+ // Forward navigation
567
+ if (indexDiff > 0 && lengthDiff === 0) {
568
+ // Check if the entry at current index is the same as before
569
+ const entryReplaced = this.hasHistoryReplacementAtIndex(this.previousEntries, entries, currentIndex);
570
+ if (entryReplaced) {
571
+ return { action: "navigate" }; // New navigation that replaced forward history
572
+ }
573
+ else {
574
+ return { action: "forward" }; // True forward navigation
575
+ }
576
+ }
577
+ // New navigation (history grew)
578
+ if (lengthDiff > 0) {
579
+ return { action: "navigate" };
580
+ }
581
+ // Same position, same length
582
+ if (lengthDiff <= 0) {
583
+ const entryReplaced = this.hasHistoryReplacementAtIndex(this.previousEntries, entries, currentIndex);
584
+ return entryReplaced ? { action: "navigate" } : { action: "reload" };
585
+ }
586
+ return { action: "unknown" };
902
587
  }
903
- return {
904
- implementedSteps,
905
- scenarios,
906
- };
907
- }
908
- async getStepsAndCommandsForScenario({ name, featureName }) {
909
- const steps = this.scenariosStepsMap.get(name) || [];
910
- for (const step of steps) {
911
- if (step.isImplemented) {
912
- step.commands = this.getCommandsForImplementedStep({ stepName: step.text });
913
- } else {
914
- step.commands = [];
915
- }
916
- }
917
- return steps;
918
- // return getStepsAndCommandsForScenario({
919
- // name,
920
- // featureName,
921
- // projectDir: this.projectDir,
922
- // map: this.scenariosStepsMap,
923
- // });
924
- }
925
-
926
- async generateStepName({ commands, stepsNames, parameters, map }) {
927
- return await this.namesService.generateStepName({ commands, stepsNames, parameters, map });
928
- }
929
- async generateScenarioAndFeatureNames(scenarioAsText) {
930
- return await this.namesService.generateScenarioAndFeatureNames(scenarioAsText);
931
- }
932
- async generateCommandName({ command }) {
933
- return await this.namesService.generateCommandName({ command });
934
- }
935
-
936
- async getCurrentChromiumPath() {
937
- const currentURL = await this.bvtContext.web.page.url();
938
- const env = JSON.parse(readFileSync(this.envName), "utf8");
939
- const baseURL = env.baseUrl;
940
- const relativeURL = currentURL.startsWith(baseURL) ? currentURL.replace(baseURL, "/") : undefined;
941
- return {
942
- relativeURL,
943
- baseURL,
944
- currentURL,
945
- };
946
- }
947
-
948
- getReportFolder() {
949
- if (this.bvtContext.reportFolder) {
950
- return this.bvtContext.reportFolder;
951
- } else return "";
952
- }
953
-
954
- getSnapshotFolder() {
955
- if (this.bvtContext.snapshotFolder) {
956
- return path.join(process.cwd(), this.bvtContext.snapshotFolder);
957
- } else return "";
958
- }
959
-
960
- async overwriteTestData(data) {
961
- this.bvtContext.stable.overwriteTestData(data.value, this.world);
962
- }
963
-
964
- _watchTestData() {
965
- this.watcher = chokidar.watch(_getDataFile(this.world, this.bvtContext, this.web), {
966
- persistent: true,
967
- ignoreInitial: true,
968
- awaitWriteFinish: {
969
- stabilityThreshold: 2000,
970
- pollInterval: 100,
971
- },
972
- });
973
-
974
- if (existsSync(_getDataFile(this.world, this.bvtContext, this.web))) {
975
- try {
976
- const testData = JSON.parse(readFileSync(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
977
- // this.logger.info("Test data", testData);
978
- this.sendEvent(this.events.getTestData, testData);
979
- } catch (e) {
980
- // this.logger.error("Error reading test data file", e);
981
- console.log("Error reading test data file", e);
982
- }
983
- }
984
-
985
- this.logger.info("Watching for test data changes");
986
-
987
- this.watcher.on("all", async (event, path) => {
988
- try {
989
- const testData = JSON.parse(await readFile(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
990
- // this.logger.info("Test data", testData);
991
- console.log("Test data changed", testData);
992
- this.sendEvent(this.events.getTestData, testData);
993
- } catch (e) {
994
- // this.logger.error("Error reading test data file", e);
995
- console.log("Error reading test data file", e);
996
- }
997
- });
998
- }
999
- async loadTestData({ data, type }) {
1000
- if (type === "user") {
1001
- const username = data.username;
1002
- await this.web.loadTestDataAsync("users", username, this.world);
1003
- } else {
1004
- const csv = data.csv;
1005
- const row = data.row;
1006
- // code = `await context.web.loadTestDataAsync("csv","${csv}:${row}", this)`;
1007
- await this.web.loadTestDataAsync("csv", `${csv}:${row}`, this.world);
1008
- }
1009
- }
1010
-
1011
- async discardTestData({ tags }) {
1012
- resetTestData(this.envName, this.world);
1013
- await this.cleanup({ tags });
1014
- }
1015
- async addToTestData(obj) {
1016
- if (!existsSync(_getDataFile(this.world, this.bvtContext, this.web))) {
1017
- await writeFile(_getDataFile(this.world, this.bvtContext, this.web), JSON.stringify({}), "utf8");
1018
- }
1019
- let data = JSON.parse(await readFile(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
1020
- data = Object.assign(data, obj);
1021
- await writeFile(_getDataFile(this.world, this.bvtContext, this.web), JSON.stringify(data), "utf8");
1022
- }
1023
- getScenarios() {
1024
- const featureFiles = readdirSync(path.join(this.projectDir, "features"))
1025
- .filter((file) => file.endsWith(".feature"))
1026
- .map((file) => path.join(this.projectDir, "features", file));
1027
- try {
1028
- const parsedFiles = featureFiles.map((file) => this.parseFeatureFile(file));
1029
- const output = {};
1030
- parsedFiles.forEach((file) => {
1031
- if (!file.feature) return;
1032
- if (!file.feature.name) return;
1033
- output[file.feature.name] = [];
1034
- file.feature.children.forEach((child) => {
1035
- if (child.scenario) {
1036
- output[file.feature.name].push(child.scenario.name);
1037
- }
588
+ async getCurrentTransition() {
589
+ if (this?.web?.browser?._name !== "chromium") {
590
+ return;
591
+ }
592
+ const client = await this.context.newCDPSession(this.web.page);
593
+ try {
594
+ const result = await client.send("Page.getNavigationHistory");
595
+ const entries = result.entries;
596
+ const currentIndex = result.currentIndex;
597
+ const currentEntry = entries[currentIndex];
598
+ const transitionInfo = this.analyzeTransitionType(entries, currentIndex, currentEntry);
599
+ this.previousIndex = currentIndex;
600
+ this.previousHistoryLength = entries.length;
601
+ this.previousUrl = currentEntry.url;
602
+ this.previousEntries = [...entries]; // Store a copy of current entries
603
+ return {
604
+ currentEntry,
605
+ navigationAction: transitionInfo.action,
606
+ };
607
+ }
608
+ catch (error) {
609
+ this.logger.error("Error in getCurrentTransition event", { error });
610
+ console.error("Error in getTransistionType event", error);
611
+ }
612
+ finally {
613
+ await client.detach();
614
+ }
615
+ }
616
+ userInitiatedTransitionTypes = ["typed", "address_bar"];
617
+ async handlePageTransition() {
618
+ const transition = await this.getCurrentTransition();
619
+ if (!transition)
620
+ return;
621
+ const { currentEntry, navigationAction } = transition;
622
+ switch (navigationAction) {
623
+ case "initial":
624
+ // console.log("Initial navigation, no action taken");
625
+ return;
626
+ case "navigate":
627
+ // console.log("transitionType", transition.transitionType);
628
+ // console.log("sending onGoto event", { url: currentEntry.url,
629
+ // type: "navigate", });
630
+ if (this.userInitiatedTransitionTypes.includes(currentEntry.transitionType)) {
631
+ const env = JSON.parse(readFileSync(this.envName, "utf8"));
632
+ const baseUrl = env.baseUrl;
633
+ let url = currentEntry.userTypedURL;
634
+ if (baseUrl && url.startsWith(baseUrl)) {
635
+ url = url.replace(baseUrl, "{{env.baseUrl}}");
636
+ }
637
+ // console.log("User initiated transition");
638
+ this.sendEvent(this.events.onGoto, { url, type: "navigate" });
639
+ }
640
+ return;
641
+ case "back":
642
+ // console.log("User navigated back");
643
+ // console.log("sending onGoto event", {
644
+ // type: "back",
645
+ // });
646
+ this.sendEvent(this.events.onGoto, { type: "back" });
647
+ return;
648
+ case "forward":
649
+ // console.log("User navigated forward"); console.log("sending onGoto event", { type: "forward", });
650
+ this.sendEvent(this.events.onGoto, { type: "forward" });
651
+ return;
652
+ default:
653
+ this.sendEvent(this.events.onGoto, { type: "unknown" });
654
+ return;
655
+ }
656
+ }
657
+ async getCurrentPageTitle() {
658
+ let title = "";
659
+ try {
660
+ title = await this.bvtContext?.page?.title();
661
+ }
662
+ catch (e) {
663
+ this.logger.error(`Error getting page title: ${getErrorMessage(e)}`);
664
+ }
665
+ return title;
666
+ }
667
+ getCurrentPageUrl() {
668
+ let url = "";
669
+ try {
670
+ url = this.bvtContext?.page?.url();
671
+ }
672
+ catch (e) {
673
+ this.logger.error(`Error getting page url: ${getErrorMessage(e)}`);
674
+ }
675
+ return url;
676
+ }
677
+ _addPagelisteners(context) {
678
+ context.on("page", async (page) => {
679
+ try {
680
+ if (page.isClosed())
681
+ return;
682
+ this.pageSet.add(page);
683
+ await page.waitForLoadState("domcontentloaded");
684
+ // add listener for frame navigation on new tab
685
+ this._addFrameNavigateListener(page);
686
+ }
687
+ catch (error) {
688
+ this.logger.error(`Error in page event: ${getErrorMessage(error)}`, undefined, "_addPagelisteners");
689
+ }
690
+ });
691
+ }
692
+ async openBrowser(_input) {
693
+ const env = JSON.parse(readFileSync(this.envName, "utf8"));
694
+ const url = env.baseUrl;
695
+ await this._initBrowser({ url });
696
+ await this._openTab({ url });
697
+ process.env.TEMP_RUN = "true";
698
+ }
699
+ overlayLocators(event) {
700
+ const locatorsResults = [...(event.locators ?? [])];
701
+ for (const cssLocator of event.cssLocators ?? []) {
702
+ locatorsResults.push({ mode: "NO_TEXT", css: cssLocator });
703
+ }
704
+ if (event.digitLocators) {
705
+ for (const digitLocator of event.digitLocators) {
706
+ locatorsResults.push({ ...digitLocator, mode: "IGNORE_DIGIT" });
707
+ }
708
+ }
709
+ if (event.contextLocator) {
710
+ locatorsResults.push({
711
+ mode: "CONTEXT",
712
+ text: event.contextLocator.texts?.[0],
713
+ css: event.contextLocator.css,
714
+ climb: event.contextLocator.climbCount,
715
+ });
716
+ }
717
+ return locatorsResults;
718
+ }
719
+ setShouldTakeScreenshot(input) {
720
+ this.shouldTakeScreenshot = input?.value;
721
+ }
722
+ async getScreenShot() {
723
+ const client = await this.context.newCDPSession(this.web.page);
724
+ try {
725
+ // Using CDP to capture the screenshot
726
+ const { data } = await client.send("Page.captureScreenshot", { format: "png" });
727
+ return data;
728
+ }
729
+ catch (error) {
730
+ this.logger.error("Error in taking browser screenshot", { error });
731
+ console.error("Error in taking browser screenshot", error);
732
+ }
733
+ finally {
734
+ await client.detach();
735
+ }
736
+ }
737
+ async storeScreenshot(event) {
738
+ try {
739
+ // const spath = path.join(__dirname, "media", `${event.inputID}.png`);
740
+ const screenshotURL = await this.getScreenShot();
741
+ if (!event.element.inputID) {
742
+ return;
743
+ }
744
+ const inputId = event.element.inputID;
745
+ if (!screenshotURL) {
746
+ return;
747
+ }
748
+ this.screenshotMap.set(inputId, screenshotURL);
749
+ // writeFileSync(spath, screenshotURL, "base64");
750
+ }
751
+ catch (error) {
752
+ this.logger.error(`Error in storeScreenshot: ${getErrorMessage(error)}`, undefined, "storeScreenshot");
753
+ }
754
+ }
755
+ async generateLocators(event) {
756
+ const snapshotDetails = event.snapshotDetails;
757
+ if (!snapshotDetails) {
758
+ throw new Error("No snapshot details found");
759
+ }
760
+ const mode = event.mode;
761
+ const inputID = event.element.inputID;
762
+ const { id, contextId, doc } = snapshotDetails;
763
+ if (!doc) {
764
+ throw new Error("Snapshot details missing document content");
765
+ }
766
+ // const selector = `[data-blinq-id="${id}"]`;
767
+ if (!this.backgroundContext) {
768
+ throw new Error("Background context not initialized");
769
+ }
770
+ const newPage = await this.backgroundContext.newPage();
771
+ const htmlDoc = doc;
772
+ await newPage.setContent(htmlDoc, { waitUntil: "domcontentloaded" });
773
+ const locatorsObj = await newPage.evaluate(([id, contextId, mode]) => {
774
+ const recorder = window.__bvt_Recorder;
775
+ const contextElement = document.querySelector(`[data-blinq-context-id="${contextId}"]`);
776
+ const el = document.querySelector(`[data-blinq-id="${id}"]`);
777
+ if (!recorder || !el) {
778
+ return { locators: [], allStrategyLocators: [] };
779
+ }
780
+ if (contextElement && recorder.locatorGenerator.toContextLocators) {
781
+ const result = recorder.locatorGenerator.toContextLocators(el, contextElement);
782
+ return result ?? { locators: [], allStrategyLocators: [] };
783
+ }
784
+ const isRecordingText = mode === "recordingText";
785
+ return recorder.locatorGenerator.getElementLocators(el, {
786
+ excludeText: isRecordingText,
787
+ });
788
+ }, [id, contextId, mode]);
789
+ // console.log(`Generated locators: for ${inputID}: `, JSON.stringify(locatorsObj));
790
+ await newPage.close();
791
+ if (event.nestFrmLoc?.children) {
792
+ locatorsObj.nestFrmLoc = event.nestFrmLoc.children;
793
+ }
794
+ this.sendEvent(this.events.updateCommand, {
795
+ locators: {
796
+ locators: locatorsObj.locators,
797
+ nestFrmLoc: locatorsObj.nestFrmLoc,
798
+ iframe_src: !event.frame.isTop ? event.frame.url : undefined,
799
+ },
800
+ allStrategyLocators: locatorsObj.allStrategyLocators,
801
+ inputID,
1038
802
  });
1039
- });
1040
-
1041
- return output;
1042
- } catch (e) {
1043
- console.log(e);
1044
- }
1045
- return {};
1046
- }
1047
- getCommandsForImplementedStep({ stepName }) {
1048
- const step_definitions = loadStepDefinitions(this.projectDir);
1049
- const stepParams = parseStepTextParameters(stepName);
1050
- return getCommandsForImplementedStep(stepName, step_definitions, stepParams).commands;
1051
- }
1052
-
1053
- loadExistingScenario({ featureName, scenarioName }) {
1054
- const step_definitions = loadStepDefinitions(this.projectDir);
1055
- const featureFilePath = path.join(this.projectDir, "features", featureName);
1056
- const gherkinDoc = this.parseFeatureFile(featureFilePath);
1057
- const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
1058
-
1059
- const steps = [];
1060
- const parameters = [];
1061
- if (scenario.examples && scenario.examples.length > 0) {
1062
- const example = scenario.examples[0];
1063
- example?.tableHeader?.cells.forEach((cell, index) => {
1064
- parameters.push({
1065
- key: cell.value,
1066
- value: unEscapeNonPrintables(example.tableBody[0].cells[index].value),
803
+ // const
804
+ }
805
+ async onAction(event) {
806
+ mixpanelTrackEvent(MIXPANEL_EVENTS.STEP_IN_SCENARIO, this.userId);
807
+ this._updateUrlPath();
808
+ // const locators = this.overlayLocators(event);
809
+ const cmdEvent = {
810
+ ...event.element,
811
+ ...transformAction(event.action, event.element, event.mode === "recordingText" || event.mode === "recordingContext", !!event.isPopupCloseClick, event.mode === "recordingHover", event.mode === "multiInspecting"),
812
+ // locators: {
813
+ // locators: event.locators,
814
+ // iframe_src: !event.frame.isTop ? event.frame.url : undefined,
815
+ // },
816
+ // allStrategyLocators: event.allStrategyLocators,
817
+ url: event.frame.url,
818
+ title: event.frame.title,
819
+ extract: {},
820
+ lastKnownUrlPath: this.lastKnownUrlPath,
821
+ };
822
+ // if (event.nestFrmLoc?.children) {
823
+ // cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
824
+ // }
825
+ // this.logger.info({ event });
826
+ if (this.shouldTakeScreenshot) {
827
+ await this.storeScreenshot(event);
828
+ }
829
+ // this.sendEvent(this.events.onNewCommand, cmdEvent);
830
+ // this._updateUrlPath();
831
+ if (event.locators) {
832
+ Object.assign(cmdEvent, {
833
+ locators: {
834
+ locators: event.locators,
835
+ iframe_src: !event.frame.isTop ? event.frame.url : undefined,
836
+ nestFrmLoc: event.nestFrmLoc?.children,
837
+ },
838
+ allStrategyLocators: event.allStrategyLocators,
839
+ });
840
+ this.sendEvent(this.events.onNewCommand, cmdEvent);
841
+ this._updateUrlPath();
842
+ }
843
+ else {
844
+ this.sendEvent(this.events.onNewCommand, cmdEvent);
845
+ this._updateUrlPath();
846
+ await this.generateLocators(event);
847
+ }
848
+ }
849
+ _updateUrlPath() {
850
+ try {
851
+ const url = this.bvtContext?.web?.page.url();
852
+ if (url && url !== "about:blank") {
853
+ this.lastKnownUrlPath = new URL(url).pathname;
854
+ }
855
+ }
856
+ catch (error) {
857
+ this.logger.error("Error in getting last known url path", { error });
858
+ }
859
+ return this.lastKnownUrlPath;
860
+ }
861
+ async closeBrowser(_input) {
862
+ delete process.env.TEMP_RUN;
863
+ await this.watcher?.close();
864
+ this.watcher = null;
865
+ this.previousIndex = null;
866
+ this.previousHistoryLength = null;
867
+ this.previousUrl = null;
868
+ this.previousEntries = null;
869
+ await closeContext();
870
+ this.pageSet.clear();
871
+ }
872
+ async reOpenBrowser(input) {
873
+ if (input && input.envName) {
874
+ this.envName = path.join(this.projectDir, "environments", input.envName + ".json");
875
+ process.env.BLINQ_ENV = this.envName;
876
+ }
877
+ await this.closeBrowser();
878
+ // logger.log("closed");
879
+ await delay(1000);
880
+ await this.openBrowser();
881
+ // logger.log("opened");
882
+ }
883
+ async getNumberOfOccurrences({ searchString, regex = false, partial = true, ignoreCase = false, tag = "*", }) {
884
+ this.isVerify = false;
885
+ //const script = `window.countStringOccurrences(${JSON.stringify(searchString)});`;
886
+ if (searchString.length === 0)
887
+ return -1;
888
+ let result = 0;
889
+ for (let i = 0; i < 3; i++) {
890
+ result = 0;
891
+ try {
892
+ // for (const page of this.context.pages()) {
893
+ const page = this.web.page;
894
+ for (const frame of page.frames()) {
895
+ try {
896
+ //scope, text1, tag1, regex1 = false, partial1, ignoreCase = true, _params: Params)
897
+ const frameResult = await this.web._locateElementByText(frame, searchString, tag, regex, partial, ignoreCase, {});
898
+ result += frameResult.elementCount;
899
+ }
900
+ catch (e) {
901
+ console.log(e);
902
+ }
903
+ }
904
+ // }
905
+ return result;
906
+ }
907
+ catch (e) {
908
+ console.log(e);
909
+ result = 0;
910
+ }
911
+ }
912
+ }
913
+ async startRecordingInput(_input) {
914
+ await this.setMode("recordingInput");
915
+ }
916
+ async stopRecordingInput(_input) {
917
+ await this.setMode("idle");
918
+ }
919
+ async startRecordingText(input) {
920
+ const isInspectMode = typeof input === "boolean" ? input : !!input?.isInspectMode;
921
+ if (isInspectMode) {
922
+ await this.setMode("inspecting");
923
+ }
924
+ else {
925
+ await this.setMode("recordingText");
926
+ }
927
+ }
928
+ async stopRecordingText(_input) {
929
+ await this.setMode("idle");
930
+ }
931
+ async startRecordingContext(_input) {
932
+ await this.setMode("recordingContext");
933
+ }
934
+ async stopRecordingContext(_input) {
935
+ await this.setMode("idle");
936
+ }
937
+ async abortExecution() {
938
+ await this.stepRunner.abortExecution();
939
+ }
940
+ async pauseExecution({ cmdId }) {
941
+ await this.stepRunner.pauseExecution(cmdId);
942
+ }
943
+ async resumeExecution({ cmdId }) {
944
+ await this.stepRunner.resumeExecution(cmdId);
945
+ }
946
+ async dealyedRevertMode() {
947
+ const timerId = setTimeout(async () => {
948
+ await this.revertMode();
949
+ }, 100);
950
+ this.timerId = timerId;
951
+ }
952
+ async runStep({ step, parametersMap, tags, isFirstStep = false, listenNetwork = false, AICode }, options) {
953
+ const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
954
+ const env = path.basename(this.envName, ".json");
955
+ const envVars = {
956
+ TOKEN: this.TOKEN,
957
+ TEMP_RUN: "true",
958
+ REPORT_FOLDER: this.bvtContext?.reportFolder,
959
+ BLINQ_ENV: this.envName,
960
+ DEBUG: "blinq:route",
961
+ };
962
+ if (!step.isImplemented) {
963
+ envVars.BVT_TEMP_SNAPSHOTS_FOLDER = path.join(this.tempSnapshotsFolder, env);
964
+ }
965
+ this.bvtContext.navigate = true;
966
+ this.bvtContext.loadedRoutes = null;
967
+ this.bvtContext.STORE_DETAILED_NETWORK_DATA = !!listenNetwork;
968
+ for (const [key, value] of Object.entries(envVars)) {
969
+ process.env[key] = value;
970
+ }
971
+ if (this.timerId) {
972
+ clearTimeout(this.timerId);
973
+ this.timerId = null;
974
+ }
975
+ await this.setMode("running");
976
+ try {
977
+ step.text = step.text.trim();
978
+ const { result, info } = await this.stepRunner.runStep({
979
+ step,
980
+ parametersMap,
981
+ envPath: this.envName,
982
+ tags,
983
+ config: this.config,
984
+ AICode,
985
+ }, this.bvtContext, {
986
+ skipAfter,
987
+ skipBefore,
988
+ });
989
+ await this.revertMode();
990
+ return { info };
991
+ }
992
+ catch (error) {
993
+ await this.revertMode();
994
+ throw error;
995
+ }
996
+ finally {
997
+ for (const key of Object.keys(envVars)) {
998
+ delete process.env[key];
999
+ }
1000
+ this.bvtContext.navigate = false;
1001
+ }
1002
+ }
1003
+ async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env, AICode, }) {
1004
+ const res = await this.workspaceService.saveScenario({
1005
+ scenario,
1006
+ featureName,
1007
+ override,
1008
+ isSingleStep,
1009
+ branch,
1010
+ isEditing,
1011
+ projectId: path.basename(this.projectDir),
1012
+ env: env ?? this.envName,
1067
1013
  });
1068
- });
1069
- }
1070
-
1071
- for (const step of scenario.steps) {
1072
- const stepParams = parseStepTextParameters(step.text);
1073
- // console.log("Parsing step ", step, stepParams);
1074
- const _s = getCommandsForImplementedStep(step.text, step_definitions, stepParams);
1075
- delete step.location;
1076
- const _step = {
1077
- ...step,
1078
- ..._s,
1079
- keyword: step.keyword.trim(),
1080
- };
1081
- parseRouteFiles(this.projectDir, _step);
1082
- steps.push(_step);
1014
+ if (res.success) {
1015
+ await this.cleanup({ tags: scenario.tags });
1016
+ }
1017
+ else {
1018
+ throw new Error(res.message || "Error saving scenario");
1019
+ }
1083
1020
  }
1084
- return {
1085
- name: scenario.name,
1086
- tags: scenario.tags.map((tag) => tag.name),
1087
- steps,
1088
- parameters,
1089
- };
1090
- }
1091
- async findRelatedTextInAllFrames({ searchString, climb, contextText, params }) {
1092
- if (searchString.length === 0) return -1;
1093
- let result = 0;
1094
- for (let i = 0; i < 3; i++) {
1095
- result = 0;
1096
- try {
1021
+ async getImplementedSteps(_input) {
1022
+ const stepsAndScenarios = await getImplementedSteps(this.projectDir);
1023
+ const implementedSteps = stepsAndScenarios.implementedSteps;
1024
+ const scenarios = stepsAndScenarios.scenarios;
1025
+ for (const scenario of scenarios) {
1026
+ this.scenariosStepsMap.set(scenario.name, scenario.steps);
1027
+ delete scenario.steps;
1028
+ }
1029
+ return {
1030
+ implementedSteps,
1031
+ scenarios,
1032
+ };
1033
+ }
1034
+ async getStepsAndCommandsForScenario({ name, featureName }) {
1035
+ const steps = this.scenariosStepsMap.get(name) || [];
1036
+ for (const step of steps) {
1037
+ if (step.isImplemented) {
1038
+ step.commands = this.getCommandsForImplementedStep({ stepName: step.text });
1039
+ }
1040
+ else {
1041
+ step.commands = [];
1042
+ }
1043
+ }
1044
+ return steps;
1045
+ }
1046
+ async generateStepName({ commands, stepsNames, parameters, map, }) {
1047
+ return await this.namesService.generateStepName({ commands, stepsNames, parameters, map });
1048
+ }
1049
+ async generateScenarioAndFeatureNames(scenarioAsText) {
1050
+ return await this.namesService.generateScenarioAndFeatureNames(scenarioAsText);
1051
+ }
1052
+ async generateCommandName({ command }) {
1053
+ return await this.namesService.generateCommandName({ command });
1054
+ }
1055
+ getCurrentChromiumPath() {
1056
+ const env = JSON.parse(readFileSync(this.envName, "utf8"));
1057
+ const baseURL = env.baseUrl;
1058
+ let currentURL = null;
1059
+ currentURL = this.bvtContext?.web?.page?.url();
1060
+ let relativeURL = undefined;
1061
+ if (typeof currentURL == "string") {
1062
+ relativeURL = currentURL.startsWith(baseURL) ? currentURL.replace(baseURL, "/") : undefined;
1063
+ }
1064
+ return {
1065
+ relativeURL,
1066
+ baseURL,
1067
+ currentURL,
1068
+ };
1069
+ }
1070
+ getReportFolder() {
1071
+ if (this.bvtContext.reportFolder) {
1072
+ return this.bvtContext.reportFolder;
1073
+ }
1074
+ else
1075
+ return "";
1076
+ }
1077
+ getSnapshotFolder() {
1078
+ if (this.bvtContext.snapshotFolder) {
1079
+ return path.join(process.cwd(), this.bvtContext.snapshotFolder);
1080
+ }
1081
+ else
1082
+ return "";
1083
+ }
1084
+ async overwriteTestData(data) {
1085
+ this.bvtContext.stable?.overwriteTestData(data.value, this.world);
1086
+ }
1087
+ _watchTestData() {
1088
+ this.watcher = chokidar.watch(_getDataFile(this.world, this.bvtContext, this.web), {
1089
+ persistent: true,
1090
+ ignoreInitial: true,
1091
+ awaitWriteFinish: {
1092
+ stabilityThreshold: 2000,
1093
+ pollInterval: 100,
1094
+ },
1095
+ });
1096
+ if (existsSync(_getDataFile(this.world, this.bvtContext, this.web))) {
1097
+ try {
1098
+ const testData = JSON.parse(readFileSync(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
1099
+ // this.logger.info("Test data", testData);
1100
+ this.sendEvent(this.events.getTestData, testData);
1101
+ }
1102
+ catch (e) {
1103
+ // this.logger.error("Error reading test data file", e);
1104
+ console.log("Error reading test data file", e);
1105
+ }
1106
+ }
1107
+ this.logger.info("Watching for test data changes");
1108
+ this.watcher.on("all", async (_event, _path) => {
1109
+ try {
1110
+ const testData = JSON.parse(await readFile(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
1111
+ // this.logger.info("Test data", testData);
1112
+ console.log("Test data changed", testData);
1113
+ this.sendEvent(this.events.getTestData, testData);
1114
+ }
1115
+ catch (e) {
1116
+ // this.logger.error("Error reading test data file", e);
1117
+ console.log("Error reading test data file", e);
1118
+ }
1119
+ });
1120
+ }
1121
+ async loadTestData({ data, type }) {
1122
+ if (type === "user") {
1123
+ const username = data.username;
1124
+ await this.web.loadTestDataAsync("users", username, this.world);
1125
+ }
1126
+ else {
1127
+ const csv = data.csv;
1128
+ const row = data.row;
1129
+ // code = `await context.web.loadTestDataAsync("csv","${csv}:${row}", this)`;
1130
+ await this.web.loadTestDataAsync("csv", `${csv}:${row}`, this.world);
1131
+ }
1132
+ }
1133
+ async discardTestData({ tags }) {
1134
+ resetTestData(this.envName, this.world);
1135
+ await this.cleanup({ tags });
1136
+ }
1137
+ async addToTestData(obj) {
1138
+ if (!existsSync(_getDataFile(this.world, this.bvtContext, this.web))) {
1139
+ await writeFile(_getDataFile(this.world, this.bvtContext, this.web), JSON.stringify({}), "utf8");
1140
+ }
1141
+ let data = JSON.parse(await readFile(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
1142
+ data = Object.assign(data, obj);
1143
+ await writeFile(_getDataFile(this.world, this.bvtContext, this.web), JSON.stringify(data), "utf8");
1144
+ }
1145
+ getScenarios() {
1146
+ const featureFiles = readdirSync(path.join(this.projectDir, "features"))
1147
+ .filter((file) => file.endsWith(".feature"))
1148
+ .map((file) => path.join(this.projectDir, "features", file));
1097
1149
  try {
1098
- const allFrameResult = await this.web.findRelatedTextInAllFrames(
1099
- contextText,
1100
- climb,
1101
- searchString,
1102
- params,
1103
- {},
1104
- this.world
1105
- );
1106
- for (const frameResult of allFrameResult) {
1107
- result += frameResult.elementCount;
1108
- }
1109
- } catch (e) {
1110
- console.log(e);
1111
- }
1112
-
1150
+ const parsedFiles = featureFiles.map((file) => this.parseFeatureFile(file));
1151
+ const output = {};
1152
+ parsedFiles.forEach((file) => {
1153
+ if (!file.feature)
1154
+ return;
1155
+ if (!file.feature.name)
1156
+ return;
1157
+ output[file.feature.name] = [];
1158
+ file.feature.children.forEach((child) => {
1159
+ if (child.scenario) {
1160
+ output[file.feature.name].push(child.scenario.name);
1161
+ }
1162
+ });
1163
+ });
1164
+ return output;
1165
+ }
1166
+ catch (e) {
1167
+ console.log(e);
1168
+ }
1169
+ return {};
1170
+ }
1171
+ getCommandsForImplementedStep({ stepName }) {
1172
+ const step_definitions = loadStepDefinitions(this.projectDir);
1173
+ const stepParams = parseStepTextParameters(stepName);
1174
+ return getCommandsForImplementedStep(stepName, step_definitions, stepParams).commands;
1175
+ }
1176
+ loadExistingScenario({ featureName, scenarioName }) {
1177
+ const step_definitions = loadStepDefinitions(this.projectDir);
1178
+ const featureFilePath = path.join(this.projectDir, "features", featureName);
1179
+ const gherkinDoc = this.parseFeatureFile(featureFilePath);
1180
+ const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
1181
+ this.scenarioDoc = scenario;
1182
+ const steps = [];
1183
+ const parameters = [];
1184
+ const datasets = [];
1185
+ if (scenario.examples && scenario.examples.length > 0) {
1186
+ const example = scenario.examples[0];
1187
+ example?.tableHeader?.cells.forEach((cell, index) => {
1188
+ parameters.push({
1189
+ key: cell.value,
1190
+ value: unEscapeNonPrintables(example.tableBody[0].cells[index].value),
1191
+ });
1192
+ // datasets.push({
1193
+ // data: example.tableBody[]
1194
+ // })
1195
+ });
1196
+ for (let i = 0; i < example.tableBody.length; i++) {
1197
+ const row = example.tableBody[i];
1198
+ // for (const row of example.tableBody) {
1199
+ const paramters = [];
1200
+ row.cells.forEach((cell, index) => {
1201
+ paramters.push({
1202
+ key: example.tableHeader.cells[index].value,
1203
+ value: unEscapeNonPrintables(cell.value),
1204
+ });
1205
+ });
1206
+ datasets.push({
1207
+ data: paramters,
1208
+ datasetId: i,
1209
+ });
1210
+ }
1211
+ }
1212
+ for (const step of scenario.steps) {
1213
+ const stepParams = parseStepTextParameters(step.text);
1214
+ // console.log("Parsing step ", step, stepParams);
1215
+ const _s = getCommandsForImplementedStep(step.text, step_definitions, stepParams);
1216
+ delete step.location;
1217
+ const _step = {
1218
+ ...step,
1219
+ ..._s,
1220
+ keyword: step.keyword.trim(),
1221
+ };
1222
+ parseRouteFiles(this.projectDir, _step);
1223
+ steps.push(_step);
1224
+ }
1225
+ return {
1226
+ name: scenario.name,
1227
+ tags: scenario.tags.map((tag) => tag.name),
1228
+ steps,
1229
+ parameters,
1230
+ datasets,
1231
+ };
1232
+ }
1233
+ async generateLocatorSummaries({ allStrategyLocators, element_name, }) {
1234
+ const input = {
1235
+ [element_name ?? "element"]: allStrategyLocators,
1236
+ };
1237
+ const result = await this.namesService.generateLocatorDescriptions({ locatorsObj: input });
1238
+ return result[element_name ?? "element"];
1239
+ }
1240
+ async findRelatedTextInAllFrames({ searchString, climb, contextText, params, }) {
1241
+ if (searchString.length === 0)
1242
+ return -1;
1243
+ let result = 0;
1244
+ for (let i = 0; i < 3; i++) {
1245
+ result = 0;
1246
+ try {
1247
+ try {
1248
+ const allFrameResult = await this.web.findRelatedTextInAllFrames(contextText, climb, searchString, params, {}, this.world);
1249
+ for (const frameResult of allFrameResult) {
1250
+ result += frameResult.elementCount;
1251
+ }
1252
+ }
1253
+ catch (e) {
1254
+ console.log(e);
1255
+ }
1256
+ return result;
1257
+ }
1258
+ catch (e) {
1259
+ console.log(e);
1260
+ result = 0;
1261
+ }
1262
+ }
1113
1263
  return result;
1114
- } catch (e) {
1115
- console.log(e);
1116
- result = 0;
1117
- }
1118
- }
1119
- return result;
1120
- }
1121
- async cleanup({ tags }) {
1122
- const noopStep = {
1123
- text: "Noop",
1124
- isImplemented: true,
1125
- };
1126
- const projectDir = this.projectDir;
1127
- console.log("Cleaning up project dir:", projectDir);
1128
-
1129
- try {
1130
- // run a dummy scenario that will run after hooks
1131
- await this.runStep(
1132
- {
1133
- step: noopStep,
1134
- parametersMap: {},
1135
- tags: tags || [],
1136
- },
1137
- {
1138
- skipAfter: false,
1139
- }
1140
- );
1141
-
1142
- // delete the temp folders (any folder that starts with __temp_features)
1143
- const tempFolders = readdirSync(projectDir).filter((folder) => folder.startsWith("__temp_features"));
1144
- for (const folder of tempFolders) {
1145
- const folderPath = path.join(projectDir, folder);
1146
- if (existsSync(folderPath)) {
1147
- this.logger.info(`Deleting temp folder: ${folderPath}`);
1148
- rmSync(folderPath, { recursive: true });
1149
- }
1150
- }
1151
- } catch (error) {
1152
- console.error("Error in cleanup", error);
1153
- }
1154
- }
1155
- async processAriaSnapshot(snapshot) {
1156
- try {
1157
- await this.evaluateInAllFrames(
1158
- this.context,
1159
- `window.__bvt_Recorder.processAriaSnapshot(${JSON.stringify(snapshot)});`
1160
- );
1161
- return true;
1162
- } catch (e) {
1163
- return false;
1164
- }
1165
- }
1166
- async deselectAriaElements() {
1167
- try {
1168
- await this.evaluateInAllFrames(this.context, `window.__bvt_Recorder.deselectAriaElements();`);
1169
- return true;
1170
- } catch (e) {
1171
- return false;
1172
- }
1173
- }
1174
- async initExecution({ tags = [] }) {
1175
- // run before hooks
1176
- const noopStep = {
1177
- text: "Noop",
1178
- isImplemented: true,
1179
- };
1180
- await this.runStep(
1181
- {
1182
- step: noopStep,
1183
- parametersMap: {},
1184
- tags,
1185
- },
1186
- {
1187
- skipBefore: false,
1188
- skipAfter: true,
1189
- }
1190
- );
1191
- }
1192
- async cleanupExecution({ tags = [] }) {
1193
- // run after hooks
1194
- const noopStep = {
1195
- text: "Noop",
1196
- isImplemented: true,
1197
- };
1198
- await this.runStep(
1199
- {
1200
- step: noopStep,
1201
- parametersMap: {},
1202
- tags,
1203
- },
1204
- {
1205
- skipBefore: true,
1206
- skipAfter: false,
1207
- }
1208
- );
1209
- }
1210
- async resetExecution({ tags = [] }) {
1211
- // run after hooks followed by before hooks
1212
- await this.cleanupExecution({ tags });
1213
- await this.initExecution({ tags });
1214
- }
1215
-
1216
- parseFeatureFile(featureFilePath) {
1217
- try {
1218
- let id = 0;
1219
- const uuidFn = () => (++id).toString(16);
1220
- const builder = new AstBuilder(uuidFn);
1221
- const matcher = new GherkinClassicTokenMatcher();
1222
- const parser = new Parser(builder, matcher);
1223
- const source = readFileSync(featureFilePath, "utf8");
1224
- const gherkinDocument = parser.parse(source);
1225
- return gherkinDocument;
1226
- } catch (e) {
1227
- this.logger.error(`Error parsing feature file: ${featureFilePath}`);
1228
- console.log(e);
1229
- }
1230
- return {};
1231
- }
1232
-
1233
- stopRecordingNetwork(input) {
1234
- if (this.bvtContext) {
1235
- this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
1236
- }
1237
- }
1264
+ }
1265
+ async setStepCodeByScenario({ function_name, mjs_file_content, user_request, selectedTarget, page_context, AIMemory, steps_context, }) {
1266
+ const runsURL = getRunsServiceBaseURL();
1267
+ const url = `${runsURL}/process-user-request/generate-code-with-context`;
1268
+ try {
1269
+ const result = await axiosClient({
1270
+ url,
1271
+ method: "POST",
1272
+ data: {
1273
+ function_name,
1274
+ mjs_file_content,
1275
+ user_request,
1276
+ selectedTarget,
1277
+ page_context,
1278
+ AIMemory,
1279
+ steps_context,
1280
+ },
1281
+ headers: {
1282
+ Authorization: `Bearer ${this.TOKEN}`,
1283
+ "X-Source": "recorder",
1284
+ },
1285
+ });
1286
+ if (result.status !== 200) {
1287
+ return { success: false, message: "Error while fetching code changes" };
1288
+ }
1289
+ return { success: true, data: result.data };
1290
+ }
1291
+ catch (error) {
1292
+ // @ts-ignore
1293
+ const reason = error?.response?.data?.error || "";
1294
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1295
+ throw new Error(`Failed to fetch code changes: ${errorMessage} \n ${reason}`);
1296
+ }
1297
+ }
1298
+ async getStepCodeByScenario({ featureName, scenarioName, projectId, branch, }) {
1299
+ try {
1300
+ const runsURL = getRunsServiceBaseURL();
1301
+ const ssoURL = runsURL.replace("/runs", "/auth");
1302
+ const privateRepoURL = `${ssoURL}/isRepoPrivate?project_id=${projectId}`;
1303
+ const isPrivateRepoReq = await axiosClient({
1304
+ url: privateRepoURL,
1305
+ method: "GET",
1306
+ headers: {
1307
+ Authorization: `Bearer ${this.TOKEN}`,
1308
+ "X-Source": "recorder",
1309
+ },
1310
+ });
1311
+ if (isPrivateRepoReq.status !== 200) {
1312
+ return { success: false, message: "Error while checking repo privacy" };
1313
+ }
1314
+ const isPrivateRepo = isPrivateRepoReq.data.isPrivate ? isPrivateRepoReq.data.isPrivate : false;
1315
+ const workspaceURL = runsURL.replace("/runs", "/workspace");
1316
+ const url = `${workspaceURL}/get-step-code-by-scenario`;
1317
+ const result = await axiosClient({
1318
+ url,
1319
+ method: "POST",
1320
+ data: {
1321
+ scenarioName,
1322
+ featureName,
1323
+ projectId,
1324
+ isPrivateRepo,
1325
+ branch,
1326
+ },
1327
+ headers: {
1328
+ Authorization: `Bearer ${this.TOKEN}`,
1329
+ "X-Source": "recorder",
1330
+ },
1331
+ });
1332
+ if (result.status !== 200) {
1333
+ return { success: false, message: "Error while getting step code" };
1334
+ }
1335
+ return { success: true, data: result.data.stepInfo };
1336
+ }
1337
+ catch (error) {
1338
+ const axiosError = error;
1339
+ const reason = axiosError?.response?.data?.error || "";
1340
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1341
+ throw new Error(`Failed to get step code: ${errorMessage} \n ${reason}`);
1342
+ }
1343
+ }
1344
+ async getContext() {
1345
+ await this.page.waitForLoadState("domcontentloaded");
1346
+ await this.page.waitForSelector("body");
1347
+ await this.page.waitForTimeout(500);
1348
+ return await this.page.evaluate(() => {
1349
+ return document.documentElement.outerHTML;
1350
+ });
1351
+ }
1352
+ async deleteCommandFromStepCode({ scenario, AICode, command }) {
1353
+ if (!AICode || AICode.length === 0) {
1354
+ console.log("No AI code available to delete.");
1355
+ return;
1356
+ }
1357
+ const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
1358
+ const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
1359
+ process.env.tempFeaturesFolderPath = __temp_features_FolderName;
1360
+ process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
1361
+ try {
1362
+ await this.stepRunner.copyCodetoTempFolder({ tempFolderPath, AICode });
1363
+ await this.stepRunner.writeWrapperCode(tempFolderPath);
1364
+ const codeView = AICode.find((f) => f.stepName === scenario.step.text);
1365
+ if (!codeView) {
1366
+ throw new Error("Step code not found for step: " + scenario.step.text);
1367
+ }
1368
+ const functionName = codeView.functionName;
1369
+ const mjsPath = path
1370
+ .normalize(codeView.mjsFile)
1371
+ .split(path.sep)
1372
+ .filter((part) => part !== "features")
1373
+ .join(path.sep);
1374
+ const codePath = path.join(tempFolderPath, mjsPath);
1375
+ if (!existsSync(codePath)) {
1376
+ throw new Error("Step code file not found: " + codePath);
1377
+ }
1378
+ const codePage = getCodePage(codePath);
1379
+ const elements = codePage.getVariableDeclarationAsObject("elements");
1380
+ const cucumberStep = getCucumberStep({ step: scenario.step });
1381
+ cucumberStep.text = scenario.step.text;
1382
+ const stepCommands = scenario.step.commands;
1383
+ const cmd = _toRecordingStep(command, scenario.step.name);
1384
+ const recording = new Recording();
1385
+ recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
1386
+ const step = { ...(recording.steps[0] ?? {}), ...(cmd ?? {}) };
1387
+ const result = _generateCodeFromCommand(step, elements, {});
1388
+ codePage._removeCommands(functionName, result.codeLines);
1389
+ codePage.removeUnusedElements();
1390
+ codePage.save();
1391
+ await rm(tempFolderPath, { recursive: true, force: true });
1392
+ return { code: codePage.fileContent, mjsFile: codeView.mjsFile };
1393
+ }
1394
+ catch (error) {
1395
+ await rm(tempFolderPath, { recursive: true, force: true });
1396
+ throw error;
1397
+ }
1398
+ }
1399
+ async addCommandToStepCode({ scenario, AICode }) {
1400
+ if (!AICode || AICode.length === 0) {
1401
+ console.log("No AI code available to add.");
1402
+ return;
1403
+ }
1404
+ const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
1405
+ const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
1406
+ process.env.tempFeaturesFolderPath = __temp_features_FolderName;
1407
+ process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
1408
+ try {
1409
+ await this.stepRunner.copyCodetoTempFolder({ tempFolderPath, AICode });
1410
+ await this.stepRunner.writeWrapperCode(tempFolderPath);
1411
+ let codeView = AICode.find((f) => f.stepName === scenario.step.text);
1412
+ if (codeView) {
1413
+ scenario.step.commands = [scenario.step.commands.pop()];
1414
+ const functionName = codeView.functionName;
1415
+ const mjsPath = path
1416
+ .normalize(codeView.mjsFile)
1417
+ .split(path.sep)
1418
+ .filter((part) => part !== "features")
1419
+ .join(path.sep);
1420
+ const codePath = path.join(tempFolderPath, mjsPath);
1421
+ if (!existsSync(codePath)) {
1422
+ throw new Error("Step code file not found: " + codePath);
1423
+ }
1424
+ const codePage = getCodePage(codePath);
1425
+ const elements = codePage.getVariableDeclarationAsObject("elements");
1426
+ const cucumberStep = getCucumberStep({ step: scenario.step });
1427
+ cucumberStep.text = scenario.step.text;
1428
+ const stepCommands = scenario.step.commands;
1429
+ const cmd = _toRecordingStep(scenario.step.commands[0], scenario.step.name);
1430
+ const recording = new Recording();
1431
+ recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
1432
+ const step = { ...(recording.steps[0] ?? {}), ...(cmd ?? {}) };
1433
+ const result = _generateCodeFromCommand(step, elements, {});
1434
+ codePage.insertElements(result.elements);
1435
+ codePage._injectOneCommand(functionName, result.codeLines.join("\n"));
1436
+ codePage.save();
1437
+ await rm(tempFolderPath, { recursive: true, force: true });
1438
+ return { code: codePage.fileContent, newStep: false, mjsFile: codeView.mjsFile };
1439
+ }
1440
+ console.log("Step code not found for step: ", scenario.step.text);
1441
+ codeView = AICode[0];
1442
+ const functionName = toMethodName(scenario.step.text);
1443
+ const codeLines = [];
1444
+ const mjsPath = path
1445
+ .normalize(codeView.mjsFile)
1446
+ .split(path.sep)
1447
+ .filter((part) => part !== "features")
1448
+ .join(path.sep);
1449
+ const codePath = path.join(tempFolderPath, mjsPath);
1450
+ if (!existsSync(codePath)) {
1451
+ throw new Error("Step code file not found: " + codePath);
1452
+ }
1453
+ const codePage = getCodePage(codePath);
1454
+ const elements = codePage.getVariableDeclarationAsObject("elements") || {};
1455
+ let newElements = { ...elements };
1456
+ const cucumberStep = getCucumberStep({ step: scenario.step });
1457
+ cucumberStep.text = scenario.step.text;
1458
+ const stepCommands = scenario.step.commands;
1459
+ stepCommands.forEach((command) => {
1460
+ const cmd = _toRecordingStep(command, scenario.step.name);
1461
+ const recording = new Recording();
1462
+ recording.loadFromObject({ steps: stepCommands, step: cucumberStep });
1463
+ const step = { ...(recording.steps[0] ?? {}), ...(cmd ?? {}) };
1464
+ const result = _generateCodeFromCommand(step, elements, {});
1465
+ const elementUpdates = result.elements ?? {};
1466
+ newElements = { ...elementUpdates };
1467
+ codeLines.push(...(result.codeLines ?? []));
1468
+ });
1469
+ codePage.insertElements(newElements);
1470
+ codePage.addInfraCommand(functionName, cucumberStep.text, cucumberStep.getVariablesList(), codeLines, false, "recorder");
1471
+ const keyword = (cucumberStep.keywordAlias ?? cucumberStep.keyword).trim();
1472
+ codePage.addCucumberStep(keyword, cucumberStep.getTemplate(), functionName, stepCommands.length);
1473
+ codePage.save();
1474
+ await rm(tempFolderPath, { recursive: true, force: true });
1475
+ return { code: codePage.fileContent, newStep: true, functionName, mjsFile: codeView.mjsFile };
1476
+ }
1477
+ catch (error) {
1478
+ await rm(tempFolderPath, { recursive: true, force: true });
1479
+ throw error;
1480
+ }
1481
+ }
1482
+ async cleanup({ tags }) {
1483
+ const noopStep = {
1484
+ text: "Noop",
1485
+ isImplemented: true,
1486
+ };
1487
+ const projectDir = this.projectDir;
1488
+ console.log("Cleaning up project dir:", projectDir);
1489
+ try {
1490
+ // run a dummy scenario that will run after hooks
1491
+ await this.runStep({
1492
+ step: noopStep,
1493
+ parametersMap: {},
1494
+ tags: tags || [],
1495
+ }, {
1496
+ skipAfter: false,
1497
+ });
1498
+ // delete the temp folders (any folder that starts with __temp_features)
1499
+ const tempFolders = readdirSync(projectDir).filter((folder) => folder.startsWith("__temp_features"));
1500
+ for (const folder of tempFolders) {
1501
+ const folderPath = path.join(projectDir, folder);
1502
+ if (existsSync(folderPath)) {
1503
+ this.logger.info(`Deleting temp folder: ${folderPath}`);
1504
+ rmSync(folderPath, { recursive: true });
1505
+ }
1506
+ }
1507
+ }
1508
+ catch (error) {
1509
+ console.error("Error in cleanup", error);
1510
+ }
1511
+ }
1512
+ async processAriaSnapshot(snapshot) {
1513
+ try {
1514
+ await this.evaluateInAllFrames(this.context, `window.__bvt_Recorder.processAriaSnapshot(${JSON.stringify(snapshot)});`);
1515
+ return true;
1516
+ }
1517
+ catch (e) {
1518
+ return false;
1519
+ }
1520
+ }
1521
+ async deselectAriaElements() {
1522
+ try {
1523
+ await this.evaluateInAllFrames(this.context, `window.__bvt_Recorder.deselectAriaElements();`);
1524
+ return true;
1525
+ }
1526
+ catch (e) {
1527
+ return false;
1528
+ }
1529
+ }
1530
+ async initExecution({ tags = [] }) {
1531
+ // run before hooks
1532
+ const noopStep = {
1533
+ text: "Noop",
1534
+ isImplemented: true,
1535
+ };
1536
+ await this.runStep({
1537
+ step: noopStep,
1538
+ parametersMap: {},
1539
+ tags,
1540
+ }, {
1541
+ skipBefore: false,
1542
+ skipAfter: true,
1543
+ });
1544
+ }
1545
+ async cleanupExecution({ tags = [] }) {
1546
+ // run after hooks
1547
+ const noopStep = {
1548
+ text: "Noop",
1549
+ isImplemented: true,
1550
+ };
1551
+ await this.runStep({
1552
+ step: noopStep,
1553
+ parametersMap: {},
1554
+ tags,
1555
+ }, {
1556
+ skipBefore: true,
1557
+ skipAfter: false,
1558
+ });
1559
+ }
1560
+ async resetExecution({ tags = [] }) {
1561
+ // run after hooks followed by before hooks
1562
+ await this.cleanupExecution({ tags });
1563
+ await this.initExecution({ tags });
1564
+ }
1565
+ parseFeatureFile(featureFilePath) {
1566
+ try {
1567
+ let id = 0;
1568
+ const uuidFn = () => (++id).toString(16);
1569
+ const builder = new AstBuilder(uuidFn);
1570
+ const matcher = new GherkinClassicTokenMatcher();
1571
+ const parser = new Parser(builder, matcher);
1572
+ const source = readFileSync(featureFilePath, "utf8");
1573
+ const gherkinDocument = parser.parse(source);
1574
+ return gherkinDocument;
1575
+ }
1576
+ catch (e) {
1577
+ this.logger.error(`Error parsing feature file: ${featureFilePath}`, { error: e });
1578
+ console.log(e);
1579
+ }
1580
+ return {};
1581
+ }
1582
+ stopRecordingNetwork(_input) {
1583
+ if (this.bvtContext) {
1584
+ this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
1585
+ }
1586
+ }
1587
+ async fakeParams(params, faker) {
1588
+ if (!faker) {
1589
+ faker = await import("@faker-js/faker/locale/en_US").then((mod) => mod.faker);
1590
+ }
1591
+ const newFakeParams = {};
1592
+ Object.keys(params).forEach((key) => {
1593
+ if (!params[key].startsWith("{{") || !params[key].endsWith("}}")) {
1594
+ newFakeParams[key] = params[key];
1595
+ return;
1596
+ }
1597
+ try {
1598
+ const value = params[key].substring(2, params[key].length - 2).trim();
1599
+ const faking = value.split("(")[0].split(".");
1600
+ let argument = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
1601
+ argument = isNaN(Number(argument)) ? argument : Number(argument);
1602
+ let fakeFunc = faker;
1603
+ faking.forEach((f) => {
1604
+ fakeFunc = fakeFunc[f];
1605
+ });
1606
+ const newValue = fakeFunc(argument);
1607
+ newFakeParams[key] = newValue;
1608
+ }
1609
+ catch (error) {
1610
+ newFakeParams[key] = params[key];
1611
+ }
1612
+ });
1613
+ return newFakeParams;
1614
+ }
1238
1615
  }