@dev-blinq/cucumber_client 1.0.1271-dev → 1.0.1271-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 (38) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +159 -14224
  2. package/bin/assets/preload/recorderv3.js +3 -1
  3. package/bin/assets/scripts/dom_parent.js +4 -0
  4. package/bin/assets/scripts/recorder.js +8 -4
  5. package/bin/assets/scripts/unique_locators.js +847 -693
  6. package/bin/assets/templates/_hooks_template.txt +37 -0
  7. package/bin/assets/templates/page_template.txt +2 -16
  8. package/bin/assets/templates/utils_template.txt +1 -46
  9. package/bin/client/apiTest/apiTest.js +6 -0
  10. package/bin/client/cli_helpers.js +11 -13
  11. package/bin/client/code_cleanup/utils.js +5 -1
  12. package/bin/client/code_gen/code_inversion.js +53 -4
  13. package/bin/client/code_gen/page_reflection.js +838 -902
  14. package/bin/client/code_gen/playwright_codeget.js +52 -13
  15. package/bin/client/cucumber/feature.js +89 -27
  16. package/bin/client/cucumber/project_to_document.js +1 -1
  17. package/bin/client/cucumber/steps_definitions.js +84 -76
  18. package/bin/client/cucumber_selector.js +17 -1
  19. package/bin/client/local_agent.js +7 -6
  20. package/bin/client/project.js +186 -196
  21. package/bin/client/recorderv3/bvt_recorder.js +171 -61
  22. package/bin/client/recorderv3/implemented_steps.js +74 -16
  23. package/bin/client/recorderv3/index.js +50 -25
  24. package/bin/client/recorderv3/network.js +299 -0
  25. package/bin/client/recorderv3/services.js +4 -16
  26. package/bin/client/recorderv3/step_runner.js +331 -67
  27. package/bin/client/recorderv3/step_utils.js +155 -5
  28. package/bin/client/recorderv3/update_feature.js +32 -30
  29. package/bin/client/recording.js +1 -0
  30. package/bin/client/run_cucumber.js +5 -1
  31. package/bin/client/scenario_report.js +0 -5
  32. package/bin/client/test_scenario.js +0 -1
  33. package/bin/client/utils/socket_logger.js +132 -0
  34. package/bin/index.js +1 -0
  35. package/bin/logger.js +3 -2
  36. package/bin/min/consoleApi.min.cjs +2 -3
  37. package/bin/min/injectedScript.min.cjs +16 -16
  38. package/package.json +24 -14
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
2
2
  import path from "path";
3
3
  import url from "url";
4
4
  import logger from "../../logger.js";
@@ -9,6 +9,8 @@ import { Step } from "../cucumber/feature.js";
9
9
  import { locateDefinitionPath, StepsDefinitions } from "../cucumber/steps_definitions.js";
10
10
  import { Recording } from "../recording.js";
11
11
  import { generateApiCode } from "../code_gen/api_codegen.js";
12
+ import { tmpdir } from "os";
13
+ import { createHash } from "crypto";
12
14
 
13
15
  const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
14
16
 
@@ -69,19 +71,75 @@ function makeStepTextUnique(step, stepsDefinitions) {
69
71
  }
70
72
 
71
73
  export async function saveRecording({ step, cucumberStep, codePage, projectDir, stepsDefinitions }) {
72
- // console.log("saveRecording", step.text);
74
+ let routesPath = path.join(tmpdir(), "blinq_temp_routes");
75
+
76
+ if (process.env.TEMP_RUN === "true") {
77
+ if (existsSync(routesPath)) {
78
+ rmSync(routesPath, { recursive: true });
79
+ }
80
+ mkdirSync(routesPath, { recursive: true });
81
+ saveRoutes({ step, folderPath: routesPath });
82
+ } else {
83
+ if (existsSync(routesPath)) {
84
+ // remove the folder
85
+ try {
86
+ rmSync(routesPath, { recursive: true });
87
+ console.log("Removed temp_routes_folder:", routesPath);
88
+ } catch (error) {
89
+ console.error("Error removing temp_routes folder", error);
90
+ }
91
+ routesPath = path.join(projectDir, "data", "routes");
92
+ if (!existsSync(routesPath)) {
93
+ mkdirSync(routesPath, { recursive: true });
94
+ }
95
+ saveRoutes({ step, folderPath: routesPath });
96
+ }
97
+ }
98
+
73
99
  if (step.isImplementedWhileRecording && !process.env.TEMP_RUN) {
74
100
  return;
75
101
  }
102
+
76
103
  if (step.isImplemented && step.shouldOverride) {
77
104
  let stepDef = stepsDefinitions.findMatchingStep(step.text);
78
105
  codePage = getCodePage(stepDef.file);
79
106
  } else {
80
107
  const isUtilStep = makeStepTextUnique(step, stepsDefinitions);
108
+
81
109
  if (isUtilStep) {
82
110
  return;
83
111
  }
84
112
  }
113
+
114
+ routesPath = path.join(tmpdir(), "blinq_temp_routes");
115
+ if (process.env.TEMP_RUN === "true") {
116
+ console.log("Save routes in temp folder for running:", routesPath);
117
+ if (existsSync(routesPath)) {
118
+ console.log("Removing existing temp_routes_folder:", routesPath);
119
+ rmSync(routesPath, { recursive: true });
120
+ }
121
+ mkdirSync(routesPath, { recursive: true });
122
+ console.log("Created temp_routes_folder:", routesPath);
123
+ saveRoutes({ step, folderPath: routesPath });
124
+ } else {
125
+ console.log("Saving routes in project directory:", projectDir);
126
+ if (existsSync(routesPath)) {
127
+ // remove the folder
128
+ try {
129
+ rmSync(routesPath, { recursive: true });
130
+ console.log("Removed temp_routes_folder:", routesPath);
131
+ } catch (error) {
132
+ console.error("Error removing temp_routes folder", error);
133
+ }
134
+ }
135
+ routesPath = path.join(projectDir, "data", "routes");
136
+ console.log("Saving routes to:", routesPath);
137
+ if (!existsSync(routesPath)) {
138
+ mkdirSync(routesPath, { recursive: true });
139
+ }
140
+ saveRoutes({ step, folderPath: routesPath });
141
+ }
142
+
85
143
  cucumberStep.text = step.text;
86
144
  const recording = new Recording();
87
145
  const steps = step.commands;
@@ -108,6 +166,7 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
108
166
  isStaticToken,
109
167
  status,
110
168
  } = step.commands[0].value;
169
+
111
170
  const result = await generateApiCode(
112
171
  {
113
172
  url,
@@ -132,6 +191,7 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
132
191
  step.keyword,
133
192
  stepsDefinitions
134
193
  );
194
+
135
195
  if (!step.isImplemented) {
136
196
  stepsDefinitions.addStep({
137
197
  name: step.text,
@@ -139,6 +199,7 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
139
199
  source: "recorder",
140
200
  });
141
201
  }
202
+
142
203
  cucumberStep.methodName = result.methodName;
143
204
  return result.codePage;
144
205
  } else {
@@ -156,17 +217,29 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
156
217
  if (step.commands && step.commands.length > 0 && step.commands[0]) {
157
218
  path = step.commands[0].lastKnownUrlPath;
158
219
  }
220
+ let protect = false;
221
+ if (step.commands && step.commands.length > 0 && step.commands[0].type) {
222
+ if (step.commands[0].type === "verify_element_property" || step.commands[0].type === "conditional_wait") {
223
+ protect = true;
224
+ }
225
+ }
159
226
  const infraResult = codePage.addInfraCommand(
160
227
  methodName,
161
228
  description,
162
229
  cucumberStep.getVariablesList(),
163
230
  generateCodeResult.codeLines,
164
- false,
231
+ protect,
165
232
  "recorder",
166
233
  path
167
234
  );
168
235
  const keyword = (cucumberStep.keywordAlias ?? cucumberStep.keyword).trim();
169
- const stepResult = codePage.addCucumberStep(keyword, cucumberStep.getTemplate(), methodName, steps.length);
236
+ const stepResult = codePage.addCucumberStep(
237
+ keyword,
238
+ cucumberStep.getTemplate(),
239
+ methodName,
240
+ steps.length,
241
+ step.finalTimeout
242
+ );
170
243
 
171
244
  if (!step.isImplemented) {
172
245
  stepsDefinitions.addStep({
@@ -177,6 +250,7 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
177
250
  }
178
251
 
179
252
  codePage.removeUnusedElements();
253
+ codePage.mergeSimilarElements();
180
254
  cucumberStep.methodName = methodName;
181
255
  if (generateCodeResult.locatorsMetadata) {
182
256
  codePage.addLocatorsMetadata(generateCodeResult.locatorsMetadata);
@@ -306,6 +380,12 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
306
380
  const utilsTemplateFilePath = path.join(__dirname, "../../assets", "templates", "utils_template.txt");
307
381
  const utilsContent = readFileSync(utilsTemplateFilePath, "utf8");
308
382
  writeFileSync(utilsFilePath, utilsContent, "utf8");
383
+ const hooksTemplateFilePath = path.join(__dirname, "../../assets", "templates", "_hooks_template.txt");
384
+ if (existsSync(hooksTemplateFilePath)) {
385
+ const hooksFilePath = path.join(stepDefinitionFolderPath, "_hooks.mjs");
386
+ const hooksContent = readFileSync(hooksTemplateFilePath, "utf8");
387
+ writeFileSync(hooksFilePath, hooksContent, "utf8");
388
+ }
309
389
  const steps = scenario.steps;
310
390
 
311
391
  const stepsDefinitions = new StepsDefinitions(projectDir);
@@ -321,6 +401,35 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
321
401
  }
322
402
  }
323
403
  if ((step.isImplemented && !step.shouldOverride) || step.commands.length === 0) {
404
+ let routesPath = path.join(tmpdir(), `blinq_temp_routes`);
405
+ if (process.env.TEMP_RUN === "true") {
406
+ console.log("Save routes in temp folder for running:", routesPath);
407
+ if (existsSync(routesPath)) {
408
+ console.log("Removing existing temp_routes_folder:", routesPath);
409
+ routesPath = path.join(tmpdir(), `blinq_temp_routes`);
410
+ rmSync(routesPath, { recursive: true });
411
+ }
412
+ mkdirSync(routesPath, { recursive: true });
413
+ console.log("Created temp_routes_folder:", routesPath);
414
+ saveRoutes({ step, folderPath: routesPath });
415
+ } else {
416
+ console.log("Saving routes in project directory:", projectDir);
417
+ if (existsSync(routesPath)) {
418
+ // remove the folder
419
+ try {
420
+ rmSync(routesPath, { recursive: true });
421
+ console.log("Removed temp_routes_folder:", routesPath);
422
+ } catch (error) {
423
+ console.error("Error removing temp_routes folder", error);
424
+ }
425
+ }
426
+ routesPath = path.join(projectDir, "data", "routes");
427
+ console.log("Saving routes to:", routesPath);
428
+ if (!existsSync(routesPath)) {
429
+ mkdirSync(routesPath, { recursive: true });
430
+ }
431
+ saveRoutes({ step, folderPath: routesPath });
432
+ }
324
433
  continue;
325
434
  }
326
435
  const cucumberStep = getCucumberStep({ step });
@@ -328,7 +437,6 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
328
437
  const stepDefsFilePath = locateDefinitionPath(featureFolder, pageName);
329
438
  // path.join(stepDefinitionFolderPath, pageName + "_page.mjs");
330
439
  let codePage = getCodePage(stepDefsFilePath);
331
-
332
440
  codePage = await saveRecording({ step, cucumberStep, codePage, projectDir, stepsDefinitions });
333
441
  if (!codePage) {
334
442
  continue;
@@ -340,3 +448,45 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
340
448
  }
341
449
  writeFileSync(utilsFilePath, utilsContent, "utf8");
342
450
  }
451
+
452
+ export function saveRoutes({ step, folderPath }) {
453
+ const routeItems = step.routeItems;
454
+ if (!routeItems || routeItems.length === 0) {
455
+ return;
456
+ }
457
+ const cucumberStep = getCucumberStep({ step });
458
+ const template = cucumberStep.getTemplate();
459
+ const stepNameHash = createHash("sha256").update(template).digest("hex");
460
+ console.log("Saving routes for step:", step.text, "with hash:", stepNameHash);
461
+
462
+ const routeItemsWithFilters = routeItems.map((routeItem) => {
463
+ const oldFilters = routeItem.filters;
464
+ const queryParamsObject = {};
465
+ oldFilters.queryParams.forEach((queryParam) => {
466
+ queryParamsObject[queryParam.key] = queryParam.value;
467
+ });
468
+ const newFilters = { path: oldFilters.path, method: oldFilters.method, queryParams: queryParamsObject };
469
+ return {
470
+ ...routeItem,
471
+ filters: newFilters,
472
+ };
473
+ });
474
+
475
+ const routesFilePath = path.join(folderPath, stepNameHash + ".json");
476
+ console.log("Routes file path:", routesFilePath);
477
+ const routesData = {
478
+ template,
479
+ routes: routeItemsWithFilters,
480
+ };
481
+ console.log("Routes data to save:", routesData);
482
+
483
+ if (!existsSync(folderPath)) {
484
+ mkdirSync(folderPath, { recursive: true });
485
+ }
486
+ try {
487
+ writeFileSync(routesFilePath, JSON.stringify(routesData, null, 2), "utf8");
488
+ console.log("Saved routes to", routesFilePath);
489
+ } catch (error) {
490
+ console.error("Failed to save routes to", routesFilePath, "Error:", error);
491
+ }
492
+ }
@@ -64,7 +64,7 @@ const escapeNonPrintables = (text) => {
64
64
  export function getCommandContent(command) {
65
65
  switch (command.type) {
66
66
  case "click_element": {
67
- return `click on ${escapeNonPrintables(command.element.name)}`;
67
+ return `${command.count === 2 ? "Double click" : "Click"} on ${escapeNonPrintables(command.element.name)}`;
68
68
  }
69
69
  case "fill_element": {
70
70
  return `fill ${escapeNonPrintables(command.element.name)} with ${escapeNonPrintables(command.parameters[0])}${command.parameters[1] ? ` and press enter key` : ""}`;
@@ -82,7 +82,7 @@ export function getCommandContent(command) {
82
82
  return `verify the element ${escapeNonPrintables(command.element.name)} contains text ${escapeNonPrintables(command.parameters[0])}`;
83
83
  }
84
84
  case "context_click": {
85
- return `click on ${escapeNonPrintables(command.label)} in the context of ${escapeNonPrintables(command.value)}`;
85
+ return `${command.count === 2 ? "Double click" : "Click"} on ${escapeNonPrintables(command.label)} in the context of ${escapeNonPrintables(command.value)}`;
86
86
  }
87
87
  case "hover_element": {
88
88
  return `hover over ${escapeNonPrintables(command.element.name)}`;
@@ -99,6 +99,9 @@ export function getCommandContent(command) {
99
99
  case "verify_page_snapshot": {
100
100
  return `verify page snapshot stored in ${command.parameters[0]}`;
101
101
  }
102
+ case "parameterized_click": {
103
+ return `${command.count === 2 ? "Parameterized double click" : "Parameterized click"} on ${escapeNonPrintables(command.element.name)}`;
104
+ }
102
105
  default: {
103
106
  return "";
104
107
  }
@@ -126,10 +129,14 @@ export function getExamplesContent(parametersMap) {
126
129
  function getTagsContent(scenario, featureFileObject) {
127
130
  let oldTags = featureFileObject?.scenarios?.find((s) => s.name === scenario.name)?.tags ?? [];
128
131
  for (const tag of scenario.tags) {
129
- if (oldTags.includes(tag)) {
130
- oldTags = oldTags.filter((t) => t !== tag);
131
- } else {
132
- oldTags.push(tag);
132
+ if (tag === "global_test_data") {
133
+ if (!oldTags.includes("global_test_data")) {
134
+ oldTags.push("global_test_data");
135
+ }
136
+ }
137
+
138
+ if (tag === "remove_global_test_data") {
139
+ oldTags = oldTags.filter((t) => t !== "global_test_data");
133
140
  }
134
141
  }
135
142
 
@@ -210,14 +217,12 @@ const GherkinToObject = (gherkin) => {
210
217
  steps: [],
211
218
  };
212
219
  while (idx < lines.length && lines[idx].startsWith("@")) {
213
- skipEmptyLines();
214
220
  const tags = [...lines[idx].matchAll(/@([^@]+)/g)].map((match) => match[1].trim());
215
221
  scenario.tags.push(...(tags ?? []));
216
222
  idx++;
223
+ skipEmptyLines();
217
224
  }
218
225
 
219
- skipEmptyLines();
220
-
221
226
  if (idx < lines.length && (lines[idx].startsWith("Scenario:") || lines[idx].startsWith("Scenario Outline:"))) {
222
227
  scenario.name = lines[idx].substring(lines[idx].indexOf(":") + 1).trim();
223
228
  idx++;
@@ -240,32 +245,32 @@ const GherkinToObject = (gherkin) => {
240
245
  !lines[idx].startsWith("@")
241
246
  ) {
242
247
  const line = lines[idx++];
243
- if (line.startsWith("#")) {
244
- const comment = line;
245
- if (comment) {
246
- const command = {
247
- type: "comment",
248
- text: comment,
249
- };
250
- scenario.steps.push(command);
251
- }
252
- } else if (line.startsWith("Examples:")) {
253
- obj.hasParams = true;
254
- const command = {
248
+ let command;
249
+ if (line.startsWith("Examples:")) {
250
+ scenario.hasParams = true;
251
+ command = {
255
252
  type: "examples",
256
253
  lines: [],
257
254
  };
258
-
259
255
  while (idx < lines.length && lines[idx].startsWith("|")) {
260
256
  const line = lines[idx++];
261
257
  command.lines.push(line);
262
258
  }
263
259
  } else {
264
- scenario.steps.push({
265
- type: "step",
266
- text: line,
267
- });
260
+ if (line.startsWith("#")) {
261
+ command = {
262
+ type: "comment",
263
+ text: line,
264
+ };
265
+ } else {
266
+ command = {
267
+ type: "step",
268
+ text: line,
269
+ };
270
+ }
268
271
  }
272
+ scenario.steps.push(command);
273
+ skipEmptyLines();
269
274
  }
270
275
 
271
276
  return scenario;
@@ -274,7 +279,6 @@ const GherkinToObject = (gherkin) => {
274
279
  while (idx < lines.length) {
275
280
  const scenario = getScenario();
276
281
  if (scenario === -1) break;
277
-
278
282
  if (scenario.error) {
279
283
  return {
280
284
  error: scenario.error,
@@ -300,8 +304,7 @@ function updateExistingScenario({ featureFileContent, scenarioName, scenarioCont
300
304
  skipScenarioIndex = i;
301
305
  continue;
302
306
  }
303
- let scenarioContent = `${featureFileObject.hasParams ? "Scenario Outline" : "Scenario"}: ${scenario.name}`;
304
-
307
+ let scenarioContent = `${scenario.hasParams ? "Scenario Outline" : "Scenario"}: ${scenario.name}`;
305
308
  let tagsLine;
306
309
  if (scenario.tags?.length > 0) {
307
310
  tagsLine = `${scenario.tags.map((t) => `@${t}`).join(" ")}`;
@@ -324,7 +327,6 @@ function updateExistingScenario({ featureFileContent, scenarioName, scenarioCont
324
327
  if (skipScenarioIndex !== -1) {
325
328
  finalContent = results.join("\n") + "\n" + scenarioContent;
326
329
  }
327
-
328
330
  return finalContent;
329
331
  }
330
332
  export async function updateFeatureFile({ featureName, scenario, override, projectDir }) {
@@ -52,6 +52,7 @@ const Types = {
52
52
  VERIFY_FILE_EXISTS: "verify_file_exists",
53
53
  SET_INPUT_FILES: "set_input_files",
54
54
  VERIFY_PAGE_SNAPSHOT: "verify_page_snapshot",
55
+ CONDITIONAL_WAIT: "conditional_wait",
55
56
  };
56
57
  class Recording {
57
58
  steps = [];
@@ -131,6 +131,10 @@ const runCucumber = async (
131
131
  } else if (!process.env.NODE_ENV_BLINQ) {
132
132
  serviceUrl = "https://logic.blinq.io";
133
133
  logger.info("Running in prod mode");
134
+ } else if (process.env.WHITELABEL === "true") {
135
+ // if it's a whitelabel it will use "api" instead of "logic"
136
+ serviceUrl = process.env.NODE_ENV_BLINQ;
137
+ logger.info("Running in custom mode: " + serviceUrl);
134
138
  } else {
135
139
  serviceUrl = process.env.NODE_ENV_BLINQ.replace("api", "logic");
136
140
  logger.info("Running in custom mode: " + serviceUrl);
@@ -355,7 +359,7 @@ const runCucumber = async (
355
359
  await aiAgent.createNewStepLocal(
356
360
  featureName,
357
361
  cucumberStep,
358
- feature,
362
+ feature.comments,
359
363
  userData,
360
364
  first,
361
365
  previousTasks,
@@ -33,17 +33,12 @@ const findNextIdInFolder = (folder) => {
33
33
  // get temp file path from --temp-file arg
34
34
  const getTempFilePath = () => {
35
35
  let tempFilePath = null;
36
-
37
36
  for (const arg of process.argv) {
38
37
  const [key, path] = arg.split("=");
39
38
  if (key === "--temp-file" && !!path) {
40
39
  tempFilePath = path;
41
40
  }
42
41
  }
43
-
44
- if (tempFilePath === null) {
45
- tempFilePath = process.env.TEMP_FILE_PATH;
46
- }
47
42
  return tempFilePath;
48
43
  };
49
44
 
@@ -267,7 +267,6 @@ try {
267
267
  }
268
268
  if (validatePath !== "null") {
269
269
  await prevrunResult.context.stable.verifyPagePath(validatePath);
270
-
271
270
  }
272
271
  //finalCompare = true;
273
272
  } catch (e) {
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @typedef {Object} SocketLoggerEventPayload
3
+ * @property {string} level Log level (e.g. "info", "warn", "error", "debug")
4
+ * @property {string} context Log context/subsystem (e.g. "BVTRecorder")
5
+ * @property {*} data The log message payload (string, object, etc)
6
+ * @property {string} timestamp ISO string when the log was emitted
7
+ * @property {number} dataSize Size of data in bytes (-1 if unknown)
8
+ */
9
+
10
+ /**
11
+ * @typedef {Object} SocketLoggerInitOptions
12
+ * @property {string=} context Default context for all logs (optional)
13
+ * @property {string=} eventName Default event name (default: "recorder.log")
14
+ */
15
+
16
+ /**
17
+ * SocketLogger - Singleton for structured socket-based logging.
18
+ *
19
+ * @namespace SocketLogger
20
+ * @property {function(import('socket.io-client').Socket|import('socket.io').Socket, SocketLoggerInitOptions=):void} init
21
+ * @property {function(string, (string|*), Object=, string=, string=):void} log
22
+ * @property {function((string|*), Object=):void} info
23
+ * @property {function((string|*), Object=):void} warn
24
+ * @property {function((string|*), Object=):void} debug
25
+ * @property {function((string|*), Object=):void} error
26
+ *
27
+ * @example
28
+ * import logger from "./socket_logger.js";
29
+ * logger.init(socket, { context: "BVTRecorder" });
30
+ * logger.info("Step started", { step: 2 });
31
+ * logger.error("Failed!", { error: "bad stuff" });
32
+ */
33
+ const SocketLogger = (function () {
34
+ /** @type {import('socket.io-client').Socket|import('socket.io').Socket|null} */
35
+ let socket = null;
36
+ /** @type {string} */
37
+ let defaultContext = "";
38
+ /** @type {string} */
39
+ let defaultEventName = "recorder.log";
40
+
41
+ /**
42
+ * Initialize the logger (call once).
43
+ * @param {import('socket.io-client').Socket|import('socket.io').Socket} sock
44
+ * @param {SocketLoggerInitOptions=} opts
45
+ */
46
+ function init(sock, opts) {
47
+ socket = sock;
48
+ defaultContext = (opts && opts.context) || "";
49
+ defaultEventName = (opts && opts.eventName) || "recorder.log";
50
+ }
51
+
52
+ /**
53
+ * Low-level log method (most users use info/warn/debug/error).
54
+ * @param {string} level Log level ("info", "warn", "debug", "error")
55
+ * @param {string|*} message The log message or object
56
+ * @param {Object=} extra Extra fields (will be merged into log payload)
57
+ * @param {string=} eventName Override event name for this log (default: "recorder.log")
58
+ * @param {string=} context Override log context for this log (default: set in init)
59
+ */
60
+ function log(level, message, extra, eventName, context) {
61
+ if (!socket || typeof socket.emit !== "function") return;
62
+ /** @type {*} */
63
+ var data = typeof message === "object" ? message : { message: message };
64
+ /** @type {number} */
65
+ var dataSize = 0;
66
+ try {
67
+ dataSize = Buffer.byteLength(JSON.stringify(data || ""), "utf8");
68
+ } catch (e) {
69
+ dataSize = -1;
70
+ }
71
+ /** @type {SocketLoggerEventPayload} */
72
+ var eventPayload = Object.assign(
73
+ {
74
+ level: level,
75
+ context: context || defaultContext,
76
+ data: data,
77
+ timestamp: new Date().toISOString(),
78
+ dataSize: dataSize,
79
+ },
80
+ extra || {}
81
+ );
82
+ // @ts-ignore
83
+ try {
84
+ if (socket) {
85
+ socket.emit(eventName || defaultEventName, eventPayload);
86
+ }
87
+ } catch (e) {
88
+ console.error("Socket logging error:", e);
89
+ console.log("Socket event payload:", eventPayload);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Write an info-level log event.
95
+ * @param {string|*} msg The message or object
96
+ * @param {Object=} ext Any extra fields/metadata
97
+ */
98
+ function info(msg, ext) {
99
+ log("info", msg, ext);
100
+ }
101
+
102
+ /**
103
+ * Write a warn-level log event.
104
+ * @param {string|*} msg The message or object
105
+ * @param {Object=} ext Any extra fields/metadata
106
+ */
107
+ function warn(msg, ext) {
108
+ log("warn", msg, ext);
109
+ }
110
+
111
+ /**
112
+ * Write a debug-level log event.
113
+ * @param {string|*} msg The message or object
114
+ * @param {Object=} ext Any extra fields/metadata
115
+ */
116
+ function debug(msg, ext) {
117
+ log("debug", msg, ext);
118
+ }
119
+
120
+ /**
121
+ * Write an error-level log event.
122
+ * @param {string|*} msg The message or object
123
+ * @param {Object=} ext Any extra fields/metadata
124
+ */
125
+ function error(msg, ext) {
126
+ log("error", msg, ext);
127
+ }
128
+
129
+ return { init, log, info, warn, debug, error };
130
+ })();
131
+
132
+ export default SocketLogger;
package/bin/index.js CHANGED
@@ -13,4 +13,5 @@ export * from "./client/cucumber/feature_data.js";
13
13
  export * from "./client/cucumber/steps_definitions.js";
14
14
  export * from "./client/profiler.js";
15
15
  export * from "./client/code_cleanup/utils.js";
16
+
16
17
  export * from "./client/code_cleanup/find_step_definition_references.js";
package/bin/logger.js CHANGED
@@ -17,8 +17,8 @@ function formatWithInspect(val) {
17
17
  let alignColorsAndTime = null;
18
18
  let fileRotateTransport = null;
19
19
  let errorFileRotateTransport = null;
20
- export let logger = null;
21
20
  function initLogger() {
21
+ let logger = null;
22
22
  try {
23
23
  alignColorsAndTime = format.combine(
24
24
  format.timestamp({
@@ -59,9 +59,10 @@ function initLogger() {
59
59
  console.log(error);
60
60
  logger = console;
61
61
  }
62
+ return logger;
62
63
  }
63
64
 
64
- initLogger();
65
+ export const logger = initLogger();
65
66
  const changeToTaskLogger = (logFile) => {
66
67
  logger.remove(fileRotateTransport);
67
68
  fileRotateTransport.filename = logFile;