@empiricalrun/test-gen 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @empiricalrun/test-gen
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5333d3b: feat: add support for creating test case using google sheet
8
+
9
+ ## 0.1.3
10
+
11
+ ### Patch Changes
12
+
13
+ - b32c561: fix: increase readability of logs
14
+
3
15
  ## 0.1.2
4
16
 
5
17
  ### Patch Changes
package/dist/bin/index.js CHANGED
@@ -7,15 +7,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const fs_extra_1 = __importDefault(require("fs-extra"));
8
8
  const openai_1 = __importDefault(require("openai"));
9
9
  const path_1 = __importDefault(require("path"));
10
- // const { scenarios } = require("./scenarios");
11
10
  const typescript_1 = __importDefault(require("typescript"));
12
11
  const prettier_1 = __importDefault(require("prettier"));
13
12
  const eslint_1 = require("eslint");
14
13
  const dotenv_1 = __importDefault(require("dotenv"));
15
- const yaml_1 = require("yaml");
14
+ const logger_1 = require("./logger");
15
+ const scenarios_1 = require("./scenarios");
16
16
  dotenv_1.default.config({
17
17
  path: [".env.local", ".env"],
18
18
  });
19
+ const logger = new logger_1.CustomLogger();
19
20
  async function readFilesInDirectory(dir = "") {
20
21
  let files = [];
21
22
  const items = await fs_extra_1.default.readdir(dir);
@@ -67,7 +68,6 @@ async function getLLMResult(instruction) {
67
68
  ],
68
69
  model: "gpt-4o",
69
70
  });
70
- console.log("generated completion");
71
71
  const response = completion.choices[0]?.message.content || "";
72
72
  return response;
73
73
  }
@@ -83,32 +83,24 @@ function validateTypescript(filePath) {
83
83
  const errors = [];
84
84
  const syntacticDiagnostics = program.getSyntacticDiagnostics(sourceFile);
85
85
  if (syntacticDiagnostics.length > 0) {
86
- console.log("Syntactic errors:");
87
86
  syntacticDiagnostics.forEach((diagnostic) => {
88
87
  const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
89
88
  const message = typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
90
- console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
89
+ logger.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
91
90
  if (typeof diagnostic.messageText === "string") {
92
91
  errors.push(diagnostic.messageText);
93
92
  }
94
93
  });
95
94
  }
96
- else {
97
- console.log("No syntactic errors.");
98
- }
99
95
  // Get and report any semantic errors
100
96
  const semanticDiagnostics = program.getSemanticDiagnostics(sourceFile);
101
97
  if (semanticDiagnostics.length > 0) {
102
- console.log("Semantic errors:");
103
98
  semanticDiagnostics.forEach((diagnostic) => {
104
- console.log(diagnostic.messageText);
105
- if (typeof diagnostic.messageText == "string") {
106
- errors.push(diagnostic.messageText);
107
- }
99
+ errors.push(diagnostic.messageText.toString());
108
100
  });
109
101
  }
110
- else {
111
- console.log("No semantic errors.");
102
+ if (!errors.length) {
103
+ logger.success("Found no type issues!");
112
104
  }
113
105
  return errors;
114
106
  }
@@ -120,7 +112,7 @@ async function formatCode(filePath) {
120
112
  filepath: filePath,
121
113
  });
122
114
  fs_extra_1.default.writeFileSync(filePath, formattedContent);
123
- console.log("File formatted successfully.");
115
+ logger.success("File formatted successfully!");
124
116
  }
125
117
  async function stripAndPrependImports(content) {
126
118
  const imports = content.match(/import.* from.*;/g);
@@ -141,12 +133,12 @@ async function generateTest(scenarios, file) {
141
133
  const pomPrompt = await generatePromptFromDirectory("./pages");
142
134
  const testFileContent = fs_extra_1.default.readFileSync(file, "utf-8");
143
135
  for (const i in scenarios) {
136
+ console.log("\n\n");
144
137
  const scenario = scenarios[i];
145
- console.log("Generating test for scenario: ", scenario?.name);
138
+ logger.log("Generating test for scenario:", scenario?.name);
146
139
  //TODO: improve this logic. its buggy
147
140
  if (testFileContent.includes(`test("${scenario?.name}"`)) {
148
- console.log("test already exists for this scenario", scenario?.name);
149
- console.log("skipping test generation");
141
+ logger.success("Test already exists for this scenario");
150
142
  continue;
151
143
  }
152
144
  const instruction = `
@@ -183,25 +175,27 @@ async function generateTest(scenarios, file) {
183
175
  - Donot repeat steps which are already mentioned in the "test.beforeEach" block
184
176
  - Add import statements at the beginning of the output.
185
177
  `;
186
- console.log("constructed instruction for llm");
187
178
  let response = await getLLMResult(instruction);
179
+ logger.success("Test generated successfully!");
188
180
  const contents = fs_extra_1.default.readFileSync(file, "utf-8");
189
181
  const [prependContent, strippedContent] = await stripAndPrependImports(response);
190
182
  await fs_extra_1.default.writeFile(file, prependContent + contents + `\n\n${strippedContent}`, "utf-8");
191
- console.log("Linting generated output");
183
+ logger.log("Linting generated code...");
192
184
  await lintErrors(file);
193
- console.log("Validating types");
185
+ logger.log("Validating types...");
194
186
  let errors = validateTypescript(file);
195
187
  const maxIteration = 2;
196
188
  let counter = 0;
197
189
  while (errors.length > 0) {
198
- console.log(errors);
199
190
  const fileContent = fs_extra_1.default.readFileSync(file, "utf-8");
200
191
  counter += 1;
201
192
  if (counter > maxIteration) {
202
- console.log(`Max iteration limit reached. Please review the file ${file} and manually fix the typescript errors.`);
193
+ logger.error(`Unable to fix typescript errors. Please review ${file} manually and fix the typescript errors. Run the test-gen command again, once errors are fixed`);
203
194
  break;
204
195
  }
196
+ logger.warn("Found few errors while validating types:");
197
+ errors.forEach(e => logger.warn(e));
198
+ logger.log("Trying to fix above errors...");
205
199
  const instruction = `
206
200
  You are a software engineer who is given a task to create test basis a scenario provided to you.
207
201
  You will be provided with current tests, fixtures and page object models for you to use and write test.
@@ -230,35 +224,26 @@ async function generateTest(scenarios, file) {
230
224
  - Donot modify anything else apart from the code required to fix typescript error
231
225
  `;
232
226
  response = await getLLMResult(instruction);
233
- console.log("fixed");
234
- console.log(response);
235
- errors = validateTypescript(file);
236
227
  await fs_extra_1.default.writeFile(file, response, "utf-8");
228
+ await lintErrors(file);
229
+ errors = validateTypescript(file);
237
230
  }
238
- console.log("formatting spec file");
239
231
  await formatCode(file);
240
232
  }
241
233
  }
242
234
  (async function main() {
243
235
  if (process.argv.length != 3) {
244
- console.log("Provide a path to scenarios path to generate test");
236
+ logger.error("Please provide path to scenarios using command:", "npx @empiricalrun/test-gen <SCENARIOS_FILE_PATH>");
245
237
  process.exit(1);
246
238
  }
247
- else {
248
- const scenariosPath = process.argv[2];
249
- const file = fs_extra_1.default.readFileSync(path_1.default.resolve(process.cwd(), scenariosPath), 'utf8');
250
- const fileName = scenariosPath.split('/').pop()?.split(".")[0];
251
- const config = (0, yaml_1.parse)(file);
252
- const scenarios = config.scenarios;
253
- const fileDir = config.dir || "./tests";
254
- const filePath = `${fileDir}/${fileName}.spec.ts`;
255
- if (fs_extra_1.default.existsSync(filePath)) {
256
- console.log("Spec file already exists, skipping generation");
257
- }
258
- else {
259
- console.log("Spec file does not exist, creating new spec file");
260
- fs_extra_1.default.createFileSync(filePath);
239
+ const scenariosPath = process.argv[2];
240
+ const testGenConfigs = await (0, scenarios_1.generateScenarios)(scenariosPath);
241
+ for (const testGenConfig of testGenConfigs) {
242
+ const specPath = testGenConfig.specPath;
243
+ if (!fs_extra_1.default.existsSync(specPath)) {
244
+ logger.log(`Creating a new spec file: ${specPath}`);
245
+ fs_extra_1.default.createFileSync(specPath);
261
246
  }
262
- await generateTest(scenarios, filePath);
247
+ await generateTest(testGenConfig.scenarios, specPath);
263
248
  }
264
249
  })();
@@ -0,0 +1,13 @@
1
+ export interface Logger {
2
+ log: (message?: string, ...optionalParams: any[]) => void;
3
+ warn: (message?: string, ...optionalParams: any[]) => void;
4
+ error: (message?: string, ...optionalParams: any[]) => void;
5
+ success: (message?: string, ...optionalParams: any[]) => void;
6
+ }
7
+ export declare class CustomLogger implements Logger {
8
+ log(message?: string, ...optionalParams: any[]): void;
9
+ warn(message?: string, ...optionalParams: any[]): void;
10
+ success(message?: string, ...optionalParams: any[]): void;
11
+ error(message?: string, ...optionalParams: any[]): void;
12
+ }
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/bin/logger/index.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IAC1D,IAAI,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IAC3D,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IAC5D,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;CAC/D;AAED,qBAAa,YAAa,YAAW,MAAM;IACzC,GAAG,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE,GAAG,EAAE;IAI9C,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE,GAAG,EAAE;IAI/C,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE,GAAG,EAAE;IAIlD,KAAK,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE,GAAG,EAAE;CAGjD"}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CustomLogger = void 0;
4
+ const picocolors_1 = require("picocolors");
5
+ class CustomLogger {
6
+ log(message, ...optionalParams) {
7
+ console.log("🪵 ", (0, picocolors_1.cyan)(message), ...optionalParams);
8
+ }
9
+ warn(message, ...optionalParams) {
10
+ console.log("🟡 ", (0, picocolors_1.yellow)(message), ...optionalParams);
11
+ }
12
+ success(message, ...optionalParams) {
13
+ console.log("✅", (0, picocolors_1.green)(message), ...optionalParams);
14
+ }
15
+ error(message, ...optionalParams) {
16
+ console.log("🚨", (0, picocolors_1.red)(message), ...optionalParams);
17
+ }
18
+ }
19
+ exports.CustomLogger = CustomLogger;
@@ -0,0 +1,7 @@
1
+ import { Scenario } from '../types';
2
+ declare function generateScenarios(scenariosPath: string): Promise<{
3
+ specPath: string;
4
+ scenarios: Scenario[];
5
+ }[]>;
6
+ export { generateScenarios };
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/bin/scenarios/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAsEpC,iBAAe,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,QAAQ,EAAE,CAAA;CAAC,EAAE,CAAC,CAM7G;AAID,OAAO,EACH,iBAAiB,EACpB,CAAA"}
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateScenarios = void 0;
7
+ const google_auth_library_1 = require("google-auth-library");
8
+ const slugify_1 = __importDefault(require("slugify"));
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const yaml_1 = require("yaml");
12
+ /**
13
+ * Method to update / add scenarios to the repo.
14
+ * @param path
15
+ * @returns updated paths of scenarios
16
+ */
17
+ async function generateScenariosUsingGsheet(path) {
18
+ const { GoogleSpreadsheet } = await import("google-spreadsheet");
19
+ const url = new URL(path);
20
+ const docId = url.pathname.split("/")[3];
21
+ const searchParams = new URLSearchParams(url.hash.split("#")[1]);
22
+ const sheetId = Number(searchParams.get("gid")) || 0;
23
+ // TODO: use oauth 2
24
+ const serviceAccountAuth = new google_auth_library_1.JWT({
25
+ email: process.env.GOOGLE_SERVICE_EMAIL,
26
+ key: process.env.GOOGLE_SERVICE_EMAIL_PRIVATE_KEY,
27
+ scopes: ['https://www.googleapis.com/auth/spreadsheets'],
28
+ });
29
+ const doc = new GoogleSpreadsheet(docId, serviceAccountAuth);
30
+ await doc.loadInfo();
31
+ const sheet = doc.sheetsById[sheetId];
32
+ const rows = await sheet.getRows();
33
+ const map = new Map();
34
+ rows.forEach(r => {
35
+ // TODO: fix for case insensitive
36
+ const category = r.get("Category");
37
+ const name = r.get("Scenario");
38
+ const steps = (r.get("Steps").split("\n")).map((s) => s.trim()).filter((s) => !!s.length);
39
+ const assert = r.get("Assert");
40
+ const specPath = category ? `./tests/${category}.spec.ts` : `./tests/${(0, slugify_1.default)(name)}.spec.ts`;
41
+ const scenario = {
42
+ steps,
43
+ name,
44
+ assert
45
+ };
46
+ if (!map.get(specPath)) {
47
+ map.set(specPath, [scenario]);
48
+ }
49
+ else {
50
+ const scenarios = map.get(specPath);
51
+ scenarios.push(scenario);
52
+ map.set(specPath, scenarios);
53
+ }
54
+ });
55
+ const results = [];
56
+ for (const [specPath, scenarios] of map.entries()) {
57
+ results.push({
58
+ specPath,
59
+ scenarios
60
+ });
61
+ }
62
+ return results;
63
+ }
64
+ async function generateScenariosUsingYAML(scenariosPath) {
65
+ const file = await fs_extra_1.default.readFile(path_1.default.resolve(process.cwd(), scenariosPath), 'utf8');
66
+ const fileName = scenariosPath.split('/').pop()?.split(".")[0];
67
+ const config = (0, yaml_1.parse)(file);
68
+ const fileDir = config.dir || "./tests";
69
+ config.specPath = `${fileDir}/${fileName}.spec.ts`;
70
+ return [config];
71
+ }
72
+ async function generateScenarios(scenariosPath) {
73
+ if (scenariosPath.startsWith("https://docs.google.com/spreadsheets")) {
74
+ return await generateScenariosUsingGsheet(scenariosPath);
75
+ }
76
+ else {
77
+ return await generateScenariosUsingYAML(scenariosPath);
78
+ }
79
+ }
80
+ exports.generateScenarios = generateScenarios;
@@ -0,0 +1,15 @@
1
+ export type FileContent = {
2
+ filePath: string;
3
+ content: string;
4
+ };
5
+ export type TestGenConfig = {
6
+ dir?: string;
7
+ specPath?: string;
8
+ scenarios: Scenario[];
9
+ };
10
+ export type Scenario = {
11
+ name: string;
12
+ steps: string[];
13
+ assert: string;
14
+ };
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/bin/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAA;AAEH,MAAM,MAAM,aAAa,GAAG;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,QAAQ,EAAE,CAAC;CACzB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAClB,CAAA"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/test-gen",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -18,8 +18,12 @@
18
18
  "dotenv": "^16.4.5",
19
19
  "eslint": "^8.57.0",
20
20
  "fs-extra": "^11.2.0",
21
+ "google-auth-library": "^9.10.0",
22
+ "google-spreadsheet": "^4.1.2",
21
23
  "openai": "^4.47.2",
24
+ "picocolors": "^1.0.1",
22
25
  "prettier": "^3.2.5",
26
+ "slugify": "^1.6.6",
23
27
  "typescript": "^5.3.3",
24
28
  "yaml": "^2.4.2"
25
29
  },