@dev-blinq/cucumber_client 1.0.1246-dev → 1.0.1246-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 (43) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +220 -0
  2. package/bin/assets/preload/recorderv3.js +5 -3
  3. package/bin/assets/preload/unique_locators.js +1 -1
  4. package/bin/assets/scripts/aria_snapshot.js +235 -0
  5. package/bin/assets/scripts/dom_attr.js +372 -0
  6. package/bin/assets/scripts/dom_element.js +0 -0
  7. package/bin/assets/scripts/dom_parent.js +185 -0
  8. package/bin/assets/scripts/event_utils.js +105 -0
  9. package/bin/assets/scripts/pw.js +7886 -0
  10. package/bin/assets/scripts/recorder.js +1147 -0
  11. package/bin/assets/scripts/snapshot_capturer.js +155 -0
  12. package/bin/assets/scripts/unique_locators.js +852 -0
  13. package/bin/assets/scripts/yaml.js +4770 -0
  14. package/bin/assets/templates/_hooks_template.txt +37 -0
  15. package/bin/assets/templates/page_template.txt +2 -16
  16. package/bin/assets/templates/utils_template.txt +44 -71
  17. package/bin/client/apiTest/apiTest.js +6 -0
  18. package/bin/client/cli_helpers.js +11 -13
  19. package/bin/client/code_cleanup/utils.js +41 -14
  20. package/bin/client/code_gen/code_inversion.js +61 -4
  21. package/bin/client/code_gen/page_reflection.js +12 -15
  22. package/bin/client/code_gen/playwright_codeget.js +55 -16
  23. package/bin/client/cucumber/feature.js +89 -27
  24. package/bin/client/cucumber/project_to_document.js +1 -1
  25. package/bin/client/cucumber/steps_definitions.js +84 -76
  26. package/bin/client/cucumber_selector.js +13 -1
  27. package/bin/client/local_agent.js +3 -3
  28. package/bin/client/project.js +7 -1
  29. package/bin/client/recorderv3/bvt_recorder.js +298 -123
  30. package/bin/client/recorderv3/implemented_steps.js +74 -16
  31. package/bin/client/recorderv3/index.js +47 -25
  32. package/bin/client/recorderv3/network.js +299 -0
  33. package/bin/client/recorderv3/services.js +3 -15
  34. package/bin/client/recorderv3/step_runner.js +322 -67
  35. package/bin/client/recorderv3/step_utils.js +152 -5
  36. package/bin/client/recorderv3/update_feature.js +66 -34
  37. package/bin/client/recording.js +3 -0
  38. package/bin/client/run_cucumber.js +5 -1
  39. package/bin/client/scenario_report.js +0 -5
  40. package/bin/client/test_scenario.js +0 -1
  41. package/bin/client/utils/socket_logger.js +132 -0
  42. package/bin/index.js +1 -0
  43. package/package.json +17 -9
@@ -1,6 +1,5 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "fs";
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
2
2
  import path from "path";
3
- import fs from "fs";
4
3
  import { generatePageName } from "../code_gen/playwright_codeget.js";
5
4
  import {
6
5
  executeStep,
@@ -9,43 +8,72 @@ import {
9
8
  getUtilsCodePage,
10
9
  loadStepDefinitions,
11
10
  saveRecording,
11
+ saveRoutes,
12
12
  } from "./step_utils.js";
13
13
  import { escapeString, getExamplesContent } from "./update_feature.js";
14
+ import fs from "fs";
14
15
  import { locateDefinitionPath } from "../cucumber/steps_definitions.js";
16
+ import { tmpdir } from "os";
15
17
 
16
- // let copiedCodeToTemp = false;
17
- async function withAbort(fn, signal) {
18
- if (!signal) {
19
- return await fn();
20
- }
21
-
22
- const abortPromise = new Promise((_, reject) => {
23
- signal.addEventListener("abort", () => reject(new Error("Aborted")), { once: true });
24
- });
25
-
26
- return await Promise.race([fn(), abortPromise]);
27
- }
28
18
  export class BVTStepRunner {
29
- #currentStepController;
19
+ #currentStepController = null;
30
20
  #port;
31
- constructor({ projectDir }) {
21
+ #lastAttemptedCmdId = null;
22
+
23
+ constructor({ projectDir, sendExecutionStatus, bvtContext }) {
32
24
  this.projectDir = projectDir;
25
+ this.sendExecutionStatus = sendExecutionStatus;
26
+ this.bvtContext = bvtContext;
27
+ this.liveExecutionMap = new Map();
33
28
  }
29
+
34
30
  setRemoteDebugPort(port) {
35
31
  this.#port = port;
36
32
  }
33
+
34
+ // Abort the current cucumber step execution by signaling the wrapper
37
35
  async abortExecution() {
36
+ if (this.bvtContext.web.pausedCmd) {
37
+ this.bvtContext.web.pausedCmd = null;
38
+ }
39
+ this.liveExecutionMap.clear();
38
40
  if (this.#currentStepController) {
39
41
  this.#currentStepController.abort();
40
42
  }
41
43
  }
42
44
 
45
+ async pauseExecution(cmdId) {
46
+ if (this.bvtContext.web) {
47
+ this.bvtContext.web.pausedCmd = {
48
+ id: cmdId,
49
+ ...this.liveExecutionMap.get(cmdId),
50
+ };
51
+ }
52
+ }
53
+
54
+ async resumeExecution(cmdId) {
55
+ if (this.bvtContext.web.pausedCmd) {
56
+ const { resolve } = this.bvtContext.web.pausedCmd;
57
+ if (resolve) {
58
+ resolve();
59
+ }
60
+ this.bvtContext.web.pausedCmd = null;
61
+ } else {
62
+ if (this.liveExecutionMap.has(cmdId)) {
63
+ const { resolve } = this.liveExecutionMap.get(cmdId);
64
+ if (resolve) {
65
+ resolve();
66
+ }
67
+ } else {
68
+ console.warn(`No paused command found for cmdId: ${cmdId}`);
69
+ }
70
+ }
71
+ }
72
+
43
73
  async copyCodetoTempFolder({ step, parametersMap, tempFolderPath }) {
44
- // const tempFolderPath = path.join(this.projectDir, "__temp_features");
45
74
  if (!fs.existsSync(tempFolderPath)) {
46
75
  fs.mkdirSync(tempFolderPath);
47
76
  }
48
- //copy all files from "./features" "./temp" folder
49
77
  if (fs.existsSync(tempFolderPath)) {
50
78
  fs.rmSync(tempFolderPath, { recursive: true });
51
79
  }
@@ -53,14 +81,13 @@ export class BVTStepRunner {
53
81
  overwrite: true,
54
82
  recursive: true,
55
83
  });
56
- // copiedCodeToTemp = true;
57
84
  }
58
85
 
59
- async writeTempFeatureFile({ step, parametersMap, tempFolderPath }) {
86
+ async writeTempFeatureFile({ step, parametersMap, tempFolderPath, tags }) {
60
87
  const tFilePath = path.join(tempFolderPath, "__temp.feature");
61
- // console.log(tFilePath);
62
88
  let tFileContent = `# temp feature file
63
89
  Feature: Temp feature
90
+ ${tags ? tags.join(" ") : ""}
64
91
  Scenario Outline: Temp Scenario
65
92
  Given ${escapeString(step.text)}
66
93
  `;
@@ -68,67 +95,295 @@ export class BVTStepRunner {
68
95
  writeFileSync(tFilePath, tFileContent);
69
96
  return tFilePath;
70
97
  }
71
- async runStep({ step, parametersMap, envPath }, bvtContext, options) {
72
- let codePage; // = getCodePage();
73
- // const tempFolderPath = process.env.tempFeaturesFolderPath;
74
- const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
75
- const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
76
- process.env.tempFeaturesFolderPath = __temp_features_FolderName;
77
- process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
78
- // if (!copiedCodeToTemp) {
79
- // await this.copyCodetoTempFolder({ step, parametersMap, tempFolderPath });
80
- // }
81
- await this.copyCodetoTempFolder({ step, parametersMap, tempFolderPath });
82
- // console.log({ feature_file_path });
83
- let stepsDefinitions = loadStepDefinitions(this.projectDir, false, true);
84
- // console.log({ stepsDefinitions });
85
- const cucumberStep = getCucumberStep({ step });
86
- if (cucumberStep.parameters && Array.isArray(cucumberStep.parameters)) {
87
- cucumberStep.parameters.forEach((param) => {
88
- if (param.variableName) {
89
- param.callValue = parametersMap[param.variableName];
90
- }
98
+
99
+ // Generate wrapper code that integrates AbortSignal into cucumber step definitions
100
+ generateWrapperCode() {
101
+ return `
102
+ import {setDefinitionFunctionWrapper} from "@dev-blinq/cucumber-js";
103
+
104
+ setDefinitionFunctionWrapper((fn) => {
105
+ return async function (...args) {
106
+ const signal = global.__BVT_STEP_ABORT_SIGNAL;
107
+ if (signal) {
108
+ signal.throwIfAborted?.();
109
+ const abortHandler = () => {
110
+ throw new Error("Aborted");
111
+ };
112
+ signal.addEventListener("abort", abortHandler, { once: true });
113
+ try {
114
+ return await fn.apply(this, args);
115
+ } finally {
116
+ signal.removeEventListener("abort", abortHandler);
117
+ }
118
+ }
119
+ return await fn.apply(this, args);
120
+ };
121
+ });
122
+
123
+ `;
124
+ }
125
+
126
+ // Write the wrapper code to temp folder
127
+ async writeWrapperCode(tempFolderPath, abortSignal) {
128
+ // tempFolderPath/step_definitions/utils.mjs -> Make a file name that follows this file but always before the next file
129
+ let fileName = "utils" + Math.random().toString(36).substring(2, 7) + ".mjs";
130
+ while (existsSync(path.join(tempFolderPath, "step_definitions", fileName))) {
131
+ fileName = "utils" + Math.random().toString(36).substring(2, 7) + ".mjs";
132
+ }
133
+ const wrapperCode = this.generateWrapperCode();
134
+
135
+ // Ensure directory exists
136
+ const stepDefinitionFolderPath = path.join(tempFolderPath, "step_definitions");
137
+ if (!existsSync(stepDefinitionFolderPath)) {
138
+ mkdirSync(stepDefinitionFolderPath, { recursive: true });
139
+ }
140
+
141
+ writeFileSync(path.join(stepDefinitionFolderPath, fileName), wrapperCode);
142
+
143
+ // Set the abort signal globally so the wrapper can access it
144
+ global.__BVT_STEP_ABORT_SIGNAL = abortSignal;
145
+
146
+ return path.join(stepDefinitionFolderPath, fileName);
147
+ }
148
+
149
+ // Execute cucumber step - simplified without abort signal handling at this level
150
+ async executeStepWithAbort({ feature_file_path, scenario, tempFolderPath, stepText, config }, options) {
151
+ const { skipAfter = true, skipBefore = true } = options || {};
152
+
153
+ const environment = { ...process.env };
154
+ const { loadConfiguration, loadSupport, runCucumber } = await import("@dev-blinq/cucumber-js/api");
155
+
156
+ const { runConfiguration } = await loadConfiguration(
157
+ {
158
+ provided: {
159
+ name: [scenario],
160
+ paths: [feature_file_path],
161
+ import: [path.join(tempFolderPath, "step_definitions", "**", "*.mjs")],
162
+ },
163
+ },
164
+ { cwd: process.cwd(), env: environment }
165
+ );
166
+
167
+ const support = await loadSupport(runConfiguration, { cwd: process.cwd(), env: environment });
168
+
169
+ support.afterTestRunHookDefinitions = [];
170
+ if (skipAfter) {
171
+ support.afterTestCaseHookDefinitions = [];
172
+ }
173
+ if (skipBefore && !config.legacySyntax) {
174
+ support.beforeTestCaseHookDefinitions = support.beforeTestCaseHookDefinitions.filter((hook) => {
175
+ return hook.uri.endsWith("_hooks.mjs");
91
176
  });
92
177
  }
178
+ support.beforeTestRunHookDefinitions = [];
93
179
 
94
- if (!step.isImplemented && step.commands.length > 0) {
95
- const pageName = generatePageName(step.startFrame?.url ?? "default");
96
- const stepDefinitionFolderPath = path.join(tempFolderPath, "step_definitions");
97
- if (!existsSync(stepDefinitionFolderPath)) {
98
- mkdirSync(stepDefinitionFolderPath, { recursive: true });
99
- }
100
- const stepDefsFilePath = locateDefinitionPath(tempFolderPath, pageName);
101
- //path.join(stepDefinitionFolderPath, pageName + "_page.mjs");
102
- codePage = getCodePage(stepDefsFilePath);
103
- codePage = await saveRecording({ step, cucumberStep, codePage, projectDir: this.projectDir, stepsDefinitions });
104
- if (codePage) {
105
- await codePage.save(stepDefsFilePath);
106
- // console.log("saved code page: ", stepDefsFilePath);
180
+ let errorMessage = null;
181
+ let info = null;
182
+ let errInfo = null;
183
+
184
+ const result = await runCucumber({ ...runConfiguration, support }, environment, (message) => {
185
+ if (message.testStepFinished) {
186
+ const { testStepFinished } = message;
187
+ const { testStepResult } = testStepFinished;
188
+ if (testStepResult.status === "FAILED" || testStepResult.status === "AMBIGUOUS") {
189
+ if (!errorMessage) {
190
+ errorMessage = testStepResult.message;
191
+ if (info) {
192
+ errInfo = info;
193
+ }
194
+ }
195
+ }
196
+ if (testStepResult.status === "UNDEFINED") {
197
+ if (!errorMessage) {
198
+ errorMessage = `step ${JSON.stringify(stepText)} is ${testStepResult.status}`;
199
+ if (info) {
200
+ errInfo = info;
201
+ }
202
+ }
203
+ }
107
204
  }
108
- if (!codePage) {
109
- codePage = getUtilsCodePage(this.projectDir);
205
+ if (message.attachment) {
206
+ const attachment = message.attachment;
207
+ if (attachment.mediaType === "application/json" && attachment.body) {
208
+ const body = JSON.parse(attachment.body);
209
+ info = body.info;
210
+ const result = body.result;
211
+
212
+ if (result.status === "PASSED") {
213
+ this.sendExecutionStatus({
214
+ type: "cmdExecutionSuccess",
215
+ cmdId: body.cmdId,
216
+ selectedStrategy: info?.selectedStrategy,
217
+ });
218
+ } else {
219
+ this.sendExecutionStatus({
220
+ type: "cmdExecutionError",
221
+ cmdId: body.cmdId,
222
+ error: {
223
+ message: result.message,
224
+ info,
225
+ },
226
+ });
227
+ }
228
+ } else if (attachment.mediaType === "application/json+intercept-results" && attachment.body) {
229
+ const body = JSON.parse(attachment.body);
230
+ if (body) {
231
+ this.sendExecutionStatus({
232
+ type: "interceptResults",
233
+ interceptResults: body,
234
+ });
235
+ }
236
+ }
110
237
  }
238
+ });
239
+
240
+ if (errorMessage) {
241
+ const bvtError = new Error(errorMessage);
242
+ Object.assign(bvtError, { info: errInfo });
243
+ throw bvtError;
111
244
  }
112
- const feature_file_path = await this.writeTempFeatureFile({ step, parametersMap, tempFolderPath });
113
- // console.log({ feature_file_path, step_text: step.text });
114
245
 
115
- const stepExecController = new AbortController();
116
- this.#currentStepController = stepExecController;
117
- await withAbort(async () => {
118
- await stepsDefinitions.executeStepRemote(
246
+ return { result, info };
247
+ }
248
+
249
+ async runStep({ step, parametersMap, envPath, tags, config }, bvtContext, options) {
250
+ // Create a new AbortController for this specific step execution
251
+ this.#currentStepController = new AbortController();
252
+ const { signal } = this.#currentStepController;
253
+
254
+ try {
255
+ this.#lastAttemptedCmdId = null;
256
+ let cmdIDs = (step.commands || []).map((cmd) => cmd.cmdId);
257
+ bvtContext.web.pausedCmd = null;
258
+
259
+ // Clear the liveExecutionMap and set up new entries for this step
260
+ this.liveExecutionMap.clear();
261
+
262
+ for (const cmdId of cmdIDs) {
263
+ this.liveExecutionMap.set(cmdId, {
264
+ resolve: () => {},
265
+ reject: () => {},
266
+ });
267
+ }
268
+
269
+ if (bvtContext.web) {
270
+ bvtContext.web.getCmdId = () => {
271
+ if (cmdIDs.length === 0) {
272
+ cmdIDs = (step.commands || []).map((cmd) => cmd.cmdId);
273
+ }
274
+ const cId = cmdIDs.shift();
275
+ this.sendExecutionStatus({
276
+ type: "cmdExecutionStart",
277
+ cmdId: cId,
278
+ });
279
+ this.#lastAttemptedCmdId = cId;
280
+ return cId;
281
+ };
282
+ }
283
+
284
+ const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
285
+ const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
286
+ process.env.tempFeaturesFolderPath = __temp_features_FolderName;
287
+ process.env.TESTCASE_REPORT_FOLDER_PATH = tempFolderPath;
288
+
289
+ await this.copyCodetoTempFolder({ step, parametersMap, tempFolderPath });
290
+
291
+ // Write abort wrapper code with this step's signal
292
+ await this.writeWrapperCode(tempFolderPath, signal);
293
+
294
+ let stepsDefinitions = loadStepDefinitions(this.projectDir, false, true);
295
+ const cucumberStep = getCucumberStep({ step });
296
+
297
+ if (cucumberStep.parameters && Array.isArray(cucumberStep.parameters)) {
298
+ cucumberStep.parameters.forEach((param) => {
299
+ if (param.variableName) {
300
+ param.callValue = parametersMap[param.variableName];
301
+ }
302
+ });
303
+ }
304
+
305
+ if (!step.isImplemented && step.commands.length > 0) {
306
+ const pageName = generatePageName(step.startFrame?.url ?? "default");
307
+ const stepDefinitionFolderPath = path.join(tempFolderPath, "step_definitions");
308
+ if (!existsSync(stepDefinitionFolderPath)) {
309
+ mkdirSync(stepDefinitionFolderPath, { recursive: true });
310
+ }
311
+ const stepDefsFilePath = locateDefinitionPath(tempFolderPath, pageName);
312
+ let codePage = getCodePage(stepDefsFilePath);
313
+ codePage = await saveRecording({
314
+ step,
315
+ cucumberStep,
316
+ codePage,
317
+ projectDir: this.projectDir,
318
+ stepsDefinitions,
319
+ });
320
+ if (codePage) {
321
+ await codePage.save(stepDefsFilePath);
322
+ }
323
+ if (!codePage) {
324
+ codePage = getUtilsCodePage(this.projectDir);
325
+ }
326
+ } else {
327
+ let routesPath = path.join(tmpdir(), `blinq_temp_routes`);
328
+ if (process.env.TEMP_RUN === "true") {
329
+ if (existsSync(routesPath)) {
330
+ rmSync(routesPath, { recursive: true });
331
+ }
332
+ mkdirSync(routesPath, { recursive: true });
333
+ saveRoutes({ step, folderPath: routesPath });
334
+ } else {
335
+ if (existsSync(routesPath)) {
336
+ try {
337
+ rmSync(routesPath, { recursive: true });
338
+ } catch (error) {
339
+ console.error("Error removing temp_routes folder", error);
340
+ }
341
+ }
342
+ routesPath = path.join(this.projectDir, "data", "routes");
343
+ if (!existsSync(routesPath)) {
344
+ mkdirSync(routesPath, { recursive: true });
345
+ }
346
+ saveRoutes({ step, folderPath: routesPath });
347
+ }
348
+ }
349
+
350
+ const feature_file_path = await this.writeTempFeatureFile({
351
+ step,
352
+ parametersMap,
353
+ tempFolderPath,
354
+ tags,
355
+ });
356
+
357
+ // Execute the cucumber step - if wrapper throws "Aborted", it will propagate up
358
+ const { result, info } = await this.executeStepWithAbort(
119
359
  {
120
360
  feature_file_path,
121
361
  tempFolderPath,
122
362
  stepText: step.text,
123
363
  scenario: "Temp Scenario",
364
+ config,
124
365
  },
125
366
  options
126
367
  );
127
- }, stepExecController.signal).finally(() => {
128
- // rm temp folder
129
- if (fs.existsSync(tempFolderPath)) {
130
- fs.rmSync(tempFolderPath, { recursive: true });
368
+
369
+ return { result, info };
370
+ } catch (error) {
371
+ if (error.message && error.message.includes("Aborted")) {
372
+ throw new Error("Aborted");
373
+ } else throw error;
374
+ } finally {
375
+ // Clean up this step's controller and global reference
376
+ this.#currentStepController = null;
377
+ global.__BVT_STEP_ABORT_SIGNAL = null;
378
+
379
+ // Clean up temp folder
380
+ const __temp_features_FolderName = process.env.tempFeaturesFolderPath;
381
+ if (__temp_features_FolderName) {
382
+ const tempFolderPath = path.join(this.projectDir, __temp_features_FolderName);
383
+ if (fs.existsSync(tempFolderPath)) {
384
+ fs.rmSync(tempFolderPath, { recursive: true });
385
+ }
131
386
  }
132
- });
387
+ }
133
388
  }
134
389
  }