@dev-blinq/cucumber_client 1.0.1184-dev → 1.0.1184-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 +220 -0
  2. package/bin/assets/preload/accessibility.js +1 -1
  3. package/bin/assets/preload/find_context.js +1 -1
  4. package/bin/assets/preload/generateSelector.js +24 -0
  5. package/bin/assets/preload/locators.js +18 -0
  6. package/bin/assets/preload/recorderv3.js +80 -9
  7. package/bin/assets/preload/unique_locators.js +24 -3
  8. package/bin/assets/scripts/aria_snapshot.js +235 -0
  9. package/bin/assets/scripts/dom_attr.js +372 -0
  10. package/bin/assets/scripts/dom_element.js +0 -0
  11. package/bin/assets/scripts/dom_parent.js +185 -0
  12. package/bin/assets/scripts/event_utils.js +105 -0
  13. package/bin/assets/scripts/pw.js +7886 -0
  14. package/bin/assets/scripts/recorder.js +1147 -0
  15. package/bin/assets/scripts/snapshot_capturer.js +155 -0
  16. package/bin/assets/scripts/unique_locators.js +844 -0
  17. package/bin/assets/scripts/yaml.js +4770 -0
  18. package/bin/assets/templates/page_template.txt +2 -16
  19. package/bin/assets/templates/utils_template.txt +65 -12
  20. package/bin/client/cli_helpers.js +0 -1
  21. package/bin/client/code_cleanup/utils.js +43 -14
  22. package/bin/client/code_gen/code_inversion.js +44 -12
  23. package/bin/client/code_gen/index.js +3 -0
  24. package/bin/client/code_gen/page_reflection.js +37 -20
  25. package/bin/client/code_gen/playwright_codeget.js +149 -43
  26. package/bin/client/cucumber/feature.js +96 -42
  27. package/bin/client/cucumber/project_to_document.js +8 -7
  28. package/bin/client/cucumber/steps_definitions.js +49 -16
  29. package/bin/client/local_agent.js +9 -7
  30. package/bin/client/operations/dump_tree.js +159 -5
  31. package/bin/client/playground/playground.js +1 -1
  32. package/bin/client/project.js +6 -2
  33. package/bin/client/recorderv3/bvt_recorder.js +279 -81
  34. package/bin/client/recorderv3/cli.js +1 -0
  35. package/bin/client/recorderv3/implemented_steps.js +111 -11
  36. package/bin/client/recorderv3/index.js +48 -4
  37. package/bin/client/recorderv3/network.js +299 -0
  38. package/bin/client/recorderv3/step_runner.js +183 -13
  39. package/bin/client/recorderv3/step_utils.js +159 -14
  40. package/bin/client/recorderv3/update_feature.js +53 -28
  41. package/bin/client/recording.js +8 -0
  42. package/bin/client/run_cucumber.js +16 -2
  43. package/bin/client/scenario_report.js +112 -55
  44. package/bin/client/test_scenario.js +0 -1
  45. package/bin/index.js +1 -0
  46. package/package.json +15 -8
  47. package/bin/client/code_gen/get_implemented_steps.js +0 -27
@@ -1,20 +1,6 @@
1
- import { Given, When, Then, After, setDefaultTimeout, Before} from "@dev-blinq/cucumber-js";
2
- import { closeContext, initContext, navigate } from "automation_model";
3
- setDefaultTimeout(60 * 1000);
4
-
5
- const path = null;
1
+ import { Given, When, Then, After, setDefaultTimeout, Before } from "@dev-blinq/cucumber-js";
2
+ import { closeContext, initContext, navigate, TestContext as context } from "automation_model";
6
3
 
7
4
  const elements = {
8
5
  };
9
6
 
10
- let context = null;
11
- Before(async function () {
12
- if (!context) {
13
- context = await initContext(path, false, false, this);
14
- }
15
- await navigate(path);
16
- });
17
- After(async function () {
18
- await closeContext();
19
- context = null;
20
- });
@@ -19,9 +19,7 @@ const elements = {};
19
19
 
20
20
  let context = null;
21
21
  Before(async function (scenario) {
22
- if (!context) {
23
- context = await initContext(url, false, false, this);
24
- }
22
+ context = await initContext(url, false, false, this);
25
23
  await navigate(url);
26
24
  await context.web.beforeScenario(this, scenario);
27
25
  });
@@ -33,20 +31,21 @@ After(async function (scenario) {
33
31
 
34
32
  BeforeStep(async function (step) {
35
33
  if (context) {
36
- await context.web.beforeStep(this, step);
34
+ await context.stable.beforeStep(this, step);
37
35
  }
38
36
  });
39
37
 
40
38
  AfterStep(async function (step) {
41
39
  if (context) {
42
- await context.web.afterStep(this, step);
40
+ await context.stable.afterStep(this, step);
43
41
  }
44
42
  });
45
43
 
44
+
46
45
  /**
47
46
  * Load test data for a user
48
47
  * @param {string} user name of the user to load test data for
49
- * @protect
48
+ * @returns
50
49
  */
51
50
  async function loadUserData(user) {
52
51
  await context.web.loadTestDataAsync("users", user, this);
@@ -60,7 +59,6 @@ async function loadUserData(user) {
60
59
  async function verifyTextExistsInPage(text) {
61
60
  await context.web.verifyTextExistInPage(text, null, this);
62
61
  }
63
-
64
62
  Then("Verify the text {string} can be found in the page", verifyTextExistsInPage);
65
63
 
66
64
  /**
@@ -69,7 +67,7 @@ Then("Verify the text {string} can be found in the page", verifyTextExistsInPage
69
67
  * @protect
70
68
  */
71
69
  async function clickOnElement(elementDescription) {
72
- await context.web.simpleClick(elementDescription, null, null, this);
70
+ await context.stable.simpleClick(elementDescription, null, null, this);
73
71
  }
74
72
  When("click on {string}", clickOnElement);
75
73
  When("click {string}", clickOnElement);
@@ -83,10 +81,11 @@ When("Click {string}", clickOnElement);
83
81
  * @protect
84
82
  */
85
83
  async function fillElement(elementDescription, value) {
86
- await context.web.simpleClickType(elementDescription, value, null, null, this);
84
+ await context.stable.simpleClickType(elementDescription, value, null, null, this);
87
85
  }
88
86
  When("fill {string} with {string}", fillElement);
89
87
  When("Fill {string} with {string}", fillElement);
88
+
90
89
  /**
91
90
  * Verify text does not exist in page
92
91
  * @param {string} text the text to verify does not exist in page
@@ -107,6 +106,24 @@ async function navigateTo(url) {
107
106
  }
108
107
  When("Navigate to {string}", navigateTo);
109
108
 
109
+ /**
110
+ * Navigate to the current page
111
+ * @protect
112
+ */
113
+ async function browserNavigateBack() {
114
+ await context.web.goBack({}, this);
115
+ }
116
+ Then("Browser navigate back", browserNavigateBack);
117
+
118
+ /**
119
+ * Navigate forward in browser history
120
+ * @protect
121
+ */
122
+ async function browserNavigateForward() {
123
+ await context.web.goForward({}, this);
124
+ }
125
+ Then("Browser navigate forward", browserNavigateForward);
126
+
110
127
  /**
111
128
  * Store browser session "<path>"
112
129
  * @param {string} filePath the file path or empty to store in the test data file
@@ -141,6 +158,7 @@ Then(
141
158
  "Identify the text {string}, climb {string} levels in the page, validate text {string} can be found in the context",
142
159
  verifyTextRelatedToText
143
160
  );
161
+
144
162
  /**
145
163
  * execute bruno single request given the bruno project is placed in a folder called bruno under the root of the cucumber project
146
164
  * @requestName the name of the bruno request file
@@ -149,7 +167,7 @@ Then(
149
167
  async function runBrunoRequest(requestName) {
150
168
  await executeBrunoRequest(requestName, {}, context, this);
151
169
  }
152
-
170
+ When("Bruno - {string}", runBrunoRequest);
153
171
  When("bruno - {string}", runBrunoRequest);
154
172
 
155
173
  /**
@@ -162,6 +180,41 @@ async function verify_the_downloaded_file_exists(fileName) {
162
180
  const downloadFile = path.join(downloadFolder, fileName);
163
181
  await verifyFileExists(downloadFile, {}, context, this);
164
182
  }
165
-
166
183
  Then("Verify the file {string} exists", { timeout: 60000 }, verify_the_downloaded_file_exists);
167
- When("Noop", async function(){})
184
+
185
+ /**
186
+ * Noop step for running only hooks
187
+ */
188
+ When("Noop", async function () {});
189
+
190
+ /**
191
+ * Verify the page url is "<url>"
192
+ * @param {string} url URL to be verified against current URL
193
+ * @protect
194
+ */
195
+ async function verify_page_url(url) {
196
+ await context.web.verifyPagePath(url, {}, this);
197
+ }
198
+ Then("Verify the page url is {string}", verify_page_url);
199
+
200
+ /**
201
+ * Verify the page title is "<title>"
202
+ * @param {string} title Title to be verified against current Title
203
+ * @protect
204
+ */
205
+ async function verify_page_title(title) {
206
+ await context.web.verifyPageTitle(title, {}, this);
207
+ }
208
+ Then("Verify the page title is {string}", verify_page_title);
209
+
210
+ /**
211
+ * Explicit wait/sleep function that pauses execution for a specified duration
212
+ * @param {duration} - Duration to sleep in milliseconds (default: 1000ms)
213
+ * @param {options} - Optional configuration object
214
+ * @param {world} - Optional world context
215
+ * @returns Promise that resolves after the specified duration
216
+ */
217
+ async function sleep(duration) {
218
+ await context.web.sleep(duration, {}, null);
219
+ }
220
+ Then("Sleep for {string} ms", { timeout: -1 }, sleep);
@@ -13,5 +13,4 @@ const loadArgs = ()=>{
13
13
  const args = process.argv.slice(2)
14
14
  return args
15
15
  }
16
-
17
16
  export { validateCLIArg, loadArgs, showUsage };
@@ -10,6 +10,8 @@ import * as t from "@babel/types";
10
10
 
11
11
  import { CucumberExpression, ParameterTypeRegistry } from "@cucumber/cucumber-expressions";
12
12
  import { existsSync, readFileSync, writeFileSync } from "fs";
13
+
14
+ import { getAiConfig } from "../code_gen/page_reflection.js";
13
15
  const STEP_KEYWORDS = new Set(["Given", "When", "Then"]);
14
16
 
15
17
  /**
@@ -286,14 +288,50 @@ export function removeUnusedElementsKeys(ast, supportFilePath) {
286
288
  }
287
289
  }
288
290
  }
289
-
291
+ export function getDefaultPrettierConfig() {
292
+ let prettierConfig = {
293
+ parser: "babel",
294
+ trailingComma: "es5",
295
+ tabWidth: 2,
296
+ semi: true,
297
+ singleQuote: false,
298
+ bracketSpacing: true,
299
+ arrowParens: "always",
300
+ embeddedLanguageFormatting: "auto",
301
+ endOfLine: "lf",
302
+ printWidth: 120,
303
+ };
304
+ // check if .prettierrc file exists
305
+ const prettierConfigPath = ".prettierrc";
306
+ if (existsSync(prettierConfigPath)) {
307
+ try {
308
+ const configContent = readFileSync(prettierConfigPath, "utf-8");
309
+ prettierConfig = JSON.parse(configContent);
310
+ } catch (error) {
311
+ console.error(`Error parsing Prettier config file: ${error}`);
312
+ }
313
+ } else {
314
+ // save the default config to .prettierrc
315
+ try {
316
+ writeFileSync(prettierConfigPath, JSON.stringify(prettierConfig, null, 2), "utf-8");
317
+ // console.log(`Created default Prettier config at ${prettierConfigPath}`);
318
+ } catch (error) {
319
+ console.error(`Error writing Prettier config file: ${error}`);
320
+ }
321
+ }
322
+ return prettierConfig;
323
+ }
290
324
  /**
291
325
  * Remove unused step definitions from a file.
292
326
  * @param {string} filePath
293
327
  * @param {Array<{keyword: string, pattern: string}>} stepDefinitions
294
328
  */
295
329
  export async function removeUnusedStepDefinitions(filePath, stepDefinitions) {
296
- const supportFilePath = filePath.replace(".mjs", ".json");
330
+ let supportFilePath = filePath.replace(".mjs", ".json");
331
+ const config = getAiConfig();
332
+ if (config && config.locatorsMetadataDir) {
333
+ supportFilePath = path.join(config.locatorsMetadataDir, path.basename(supportFilePath));
334
+ }
297
335
  const ast = await parse(filePath);
298
336
  removeStepDefinitions(stepDefinitions, ast);
299
337
  removeUnusedDeclarations(ast);
@@ -301,19 +339,10 @@ export async function removeUnusedStepDefinitions(filePath, stepDefinitions) {
301
339
  // removeUnusedImports(ast);
302
340
  let code = generateCode(ast);
303
341
 
342
+ // configuration object
343
+ const prettierConfig = getDefaultPrettierConfig();
304
344
  // Format the code using Prettier
305
- code = await prettier.format(code, {
306
- parser: "babel",
307
- trailingComma: "es5",
308
- tabWidth: 2,
309
- semi: true,
310
- singleQuote: false,
311
- bracketSpacing: true,
312
- arrowParens: "always",
313
- embeddedLanguageFormatting: "auto",
314
- endOfLine: "lf",
315
- printWidth: 120,
316
- });
345
+ code = await prettier.format(code, prettierConfig);
317
346
 
318
347
  await fs.writeFile(filePath, code, "utf-8");
319
348
  console.log(`Removed unused step definitions from ${filePath}`);
@@ -109,13 +109,13 @@ const invertStableCommand = (call, elements, stepParams) => {
109
109
 
110
110
  case "click":
111
111
  // Handle different click scenarios
112
+ step.type = Types.CLICK;
113
+ step.element = extractElement(call.arguments[0]);
112
114
  if (call.arguments.length > 2 && call.arguments[2]?.type === "ObjectExpression") {
113
115
  // Context click
114
- step.type = "context_click";
115
- step.element = extractElement(call.arguments[0]);
116
-
117
116
  const contextProp = call.arguments[2].properties.find((prop) => prop.key.name === "context");
118
117
  if (contextProp) {
118
+ step.type = "context_click";
119
119
  const contextValue = parseDataSource(contextProp.value, stepParams);
120
120
  if (contextValue.type === "literal") {
121
121
  step.value = contextValue.value;
@@ -125,9 +125,10 @@ const invertStableCommand = (call, elements, stepParams) => {
125
125
  step.value = toVariableName(contextValue.dataKey);
126
126
  }
127
127
  }
128
- } else {
129
- step.type = Types.CLICK;
130
- step.element = extractElement(call.arguments[0]);
128
+ const clickCountProp = call.arguments[2].properties.find((prop) => prop.key.name === "clickCount");
129
+ if (clickCountProp) {
130
+ step.count = clickCountProp.value.value;
131
+ }
131
132
  }
132
133
  break;
133
134
 
@@ -309,6 +310,14 @@ const invertStableCommand = (call, elements, stepParams) => {
309
310
  break;
310
311
  }
311
312
 
313
+ case "goBack":
314
+ step.type = Types.GO_BACK;
315
+ break;
316
+
317
+ case "goForward":
318
+ step.type = Types.GO_FORWARD;
319
+ break;
320
+
312
321
  case "reloadPage":
313
322
  step.type = Types.RELOAD;
314
323
  break;
@@ -320,14 +329,14 @@ const invertStableCommand = (call, elements, stepParams) => {
320
329
  case "simpleClick":
321
330
  // step.type = Types.CLICK_SIMPLE;
322
331
  // step.elementDescription = call.arguments[0].value;
323
- throw new Error("simpleClick action is not supported in the recorder");
332
+ throw new Error("Action is not supported in the recorder");
324
333
 
325
334
  case "simpleClickType":
326
335
  // step.type = Types.FILL_SIMPLE;
327
336
  // step.elementDescription = call.arguments[0].value;
328
337
  // step.value = call.arguments[1].value;
329
338
  // break;
330
- throw new Error("simpleClickType action is not supported in the recorder");
339
+ throw new Error("Action is not supported in the recorder");
331
340
 
332
341
  case "hover":
333
342
  step.type = Types.HOVER;
@@ -384,7 +393,6 @@ const invertStableCommand = (call, elements, stepParams) => {
384
393
  }
385
394
  break;
386
395
  }
387
-
388
396
  case "snapshotValidation": {
389
397
  step.type = Types.VERIFY_PAGE_SNAPSHOT;
390
398
  const inputParam = parseDataSource(call.arguments[1], stepParams);
@@ -394,6 +402,30 @@ const invertStableCommand = (call, elements, stepParams) => {
394
402
  step.selectors = call.arguments[0].value;
395
403
  break;
396
404
  }
405
+ case "verifyPageTitle": {
406
+ step.type = Types.VERIFY_PAGE_TITLE;
407
+ const text = parseDataSource(call.arguments[0], stepParams);
408
+ if (text.type === "literal") {
409
+ step.parameters = [text.value];
410
+ } else {
411
+ step.dataSource = text.dataSource;
412
+ step.dataKey = text.dataKey;
413
+ step.parameters = [toVariableName(text.dataKey)];
414
+ }
415
+ break;
416
+ }
417
+ case "verifyPagePath": {
418
+ step.type = Types.VERIFY_PAGE_PATH;
419
+ const path = parseDataSource(call.arguments[0], stepParams);
420
+ if (path.type === "literal") {
421
+ step.parameters = [path.value];
422
+ } else {
423
+ step.dataSource = path.dataSource;
424
+ step.dataKey = path.dataKey;
425
+ step.parameters = [toVariableName(path.dataKey)];
426
+ }
427
+ break;
428
+ }
397
429
  default:
398
430
  return; // Skip if no matching method
399
431
  }
@@ -521,9 +553,9 @@ const invertCodeToCommand = (codeString, elements = {}, stepParams, stepsDefinit
521
553
  if (propName === "web" || propName === "stable") {
522
554
  const step = invertStableCommand(call, elements, stepParams);
523
555
  if (step) steps.push(step);
524
- // } else if (propName === "api") {
525
- // const step = invertApiCommand(stepsDefinitions, codePage, stepName);
526
- // if (step) steps.push(step);
556
+ } else if (propName === "api") {
557
+ const step = invertApiCommand(stepsDefinitions, codePage, stepName);
558
+ if (step) steps.push(step);
527
559
  } else {
528
560
  return;
529
561
  }
@@ -58,6 +58,9 @@ for (let i = 0; i < scenarioReport.stepsProgress.length; i++) {
58
58
  page.addCucumberStep(keyword, step.cucumberLine, methodName, step.recording.steps.length);
59
59
 
60
60
  page.removeUnusedElements();
61
+ if (generateCodeResult.locatorsMetadata) {
62
+ page.addLocatorsMetadata(generateCodeResult.locatorsMetadata);
63
+ }
61
64
  await page.save();
62
65
  set.add(page.sourceFileName);
63
66
  }
@@ -6,6 +6,7 @@ import logger from "../../logger.js";
6
6
  import { convertToIdentifier } from "./utils.js";
7
7
  import prettier from "prettier";
8
8
  import url from "url";
9
+ import { getDefaultPrettierConfig } from "../code_cleanup/utils.js";
9
10
  const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
10
11
  const CodeStatus = {
11
12
  ADD: "add",
@@ -22,6 +23,21 @@ function unescapeFromComment(text) {
22
23
  .replace(/\*\\/g, "*/") // Unescape comment-closing sequence
23
24
  .replace(/\\\\/g, "\\"); // Unescape backslashes
24
25
  }
26
+ let ai_config = null;
27
+ export function getAiConfig() {
28
+ if (ai_config) {
29
+ return ai_config;
30
+ }
31
+ try {
32
+ ai_config = JSON.parse(readFileSync("ai_config.json", "utf8"));
33
+ } catch (e) {
34
+ ai_config = {};
35
+ }
36
+ if (!ai_config.locatorsMetadataDir) {
37
+ ai_config.locatorsMetadataDir = "features/step_definitions/locators";
38
+ }
39
+ return ai_config;
40
+ }
25
41
  class CodePage {
26
42
  constructor(sourceFileName = null) {
27
43
  this.sourceFileName = sourceFileName;
@@ -45,18 +61,7 @@ class CodePage {
45
61
  if (this.sourceFileName !== null) {
46
62
  // format the code before saving
47
63
  try {
48
- const fileContentNew = await prettier.format(this.fileContent, {
49
- parser: "babel",
50
- trailingComma: "es5",
51
- tabWidth: 2,
52
- semi: true,
53
- singleQuote: false,
54
- bracketSpacing: true,
55
- arrowParens: "always",
56
- embeddedLanguageFormatting: "auto",
57
- endOfLine: "lf",
58
- printWidth: 120,
59
- });
64
+ const fileContentNew = await prettier.format(this.fileContent, getDefaultPrettierConfig());
60
65
  this._init();
61
66
  this.generateModel(fileContentNew);
62
67
  } catch (e) {
@@ -224,25 +229,28 @@ this.imports[2].node.source.value
224
229
  params = paramsObj.map((param) => param.name);
225
230
  }
226
231
  firstFind = false;
227
- }
232
+ }
228
233
  stepPaths.push(method.path);
229
-
230
234
  }
231
235
  }
232
236
  if (foundMethod) {
233
- templates.push({ pattern, methodName, params, stepType, paths: stepPaths});
237
+ templates.push({ pattern, methodName, params, stepType, paths: stepPaths });
234
238
  }
235
239
  }
236
240
  }
237
241
  return templates;
238
242
  }
239
- getExpectedTimeout(expectedNumofCmds) {
243
+ getExpectedTimeout(expectedNumofCmds, finalTimeout) {
244
+ const timeoutNum = parseFloat(finalTimeout);
245
+ if (finalTimeout && !isNaN(timeoutNum)) {
246
+ return -1;
247
+ }
240
248
  return expectedNumofCmds * 60 * 1000;
241
249
  }
242
- addCucumberStep(type, cucumberLine, method, expectedNumofCmds) {
250
+ addCucumberStep(type, cucumberLine, method, expectedNumofCmds, finalTimeout) {
243
251
  const result = {};
244
252
  let code = "\n";
245
- code += `${type}(${JSON.stringify(cucumberLine)}, ${expectedNumofCmds ? `{ timeout: ${this.getExpectedTimeout(expectedNumofCmds)}}, ` : ""}${method});\n`;
253
+ code += `${type}(${JSON.stringify(cucumberLine)}, ${expectedNumofCmds ? `{ timeout: ${this.getExpectedTimeout(expectedNumofCmds, finalTimeout)}}, ` : ""}${method});\n`;
246
254
  let existCodePart = null;
247
255
  for (let i = 0; i < this.cucumberCalls.length; i++) {
248
256
  if (
@@ -481,7 +489,16 @@ this.imports[2].node.source.value
481
489
  }
482
490
  addLocatorsMetadata(locatorsMetadata) {
483
491
  // create a file name based on the source file name replace .mjs with .json
484
- const locatorsMetadataFileName = this.sourceFileName.replace(".mjs", ".json");
492
+ let locatorsMetadataFileName = this.sourceFileName.replace(".mjs", ".json");
493
+ const config = getAiConfig();
494
+ if (config && config.locatorsMetadataDir) {
495
+ // if config.locatorsMetadataDir is set, use it to create the file path
496
+ locatorsMetadataFileName = path.join(config.locatorsMetadataDir, path.basename(locatorsMetadataFileName));
497
+ // check if the directory exists, if not create it
498
+ if (!existsSync(path.dirname(locatorsMetadataFileName))) {
499
+ mkdirSync(path.dirname(locatorsMetadataFileName), { recursive: true });
500
+ }
501
+ }
485
502
  let metadata = {};
486
503
  // try to read the file to metadata, protect with try catch
487
504
  try {
@@ -782,7 +799,7 @@ function getPath(comment) {
782
799
  if (index === -1) {
783
800
  return null;
784
801
  }
785
- return comment.substring(index).split('\n')[0].substring(6);
802
+ return comment.substring(index).split("\n")[0].substring(6);
786
803
  }
787
804
 
788
805
  class CodePart {