@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 +12 -0
- package/dist/bin/index.js +29 -44
- package/dist/bin/logger/index.d.ts +13 -0
- package/dist/bin/logger/index.d.ts.map +1 -0
- package/dist/bin/logger/index.js +19 -0
- package/dist/bin/scenarios/index.d.ts +7 -0
- package/dist/bin/scenarios/index.d.ts.map +1 -0
- package/dist/bin/scenarios/index.js +80 -0
- package/dist/bin/types/index.d.ts +15 -0
- package/dist/bin/types/index.d.ts.map +1 -0
- package/dist/bin/types/index.js +2 -0
- package/package.json +5 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
if (typeof diagnostic.messageText == "string") {
|
|
106
|
-
errors.push(diagnostic.messageText);
|
|
107
|
-
}
|
|
99
|
+
errors.push(diagnostic.messageText.toString());
|
|
108
100
|
});
|
|
109
101
|
}
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
+
logger.log("Linting generated code...");
|
|
192
184
|
await lintErrors(file);
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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,
|
|
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 @@
|
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empiricalrun/test-gen",
|
|
3
|
-
"version": "0.
|
|
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
|
},
|