@empiricalrun/test-gen 0.1.1
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 +13 -0
- package/README.md +24 -0
- package/dist/bin/index.d.ts +3 -0
- package/dist/bin/index.d.ts.map +1 -0
- package/dist/bin/index.js +264 -0
- package/package.json +34 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# test-gen
|
|
2
|
+
|
|
3
|
+
Package to generate e2e Playwright tests.
|
|
4
|
+
|
|
5
|
+
## Usages
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npx @empiricalrun/test-gen ./scenarios/create-flow.yaml
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Sample test scenarios
|
|
12
|
+
|
|
13
|
+
```yaml ./tests/scenarios.yaml
|
|
14
|
+
dir: ./tests/ # [optional] directory where you want to create tests. Default - ./tests
|
|
15
|
+
scenarios: # list of scenarios
|
|
16
|
+
- name: user should be able to login successfully # name of the test case to be generated
|
|
17
|
+
steps: # steps for the test case
|
|
18
|
+
- cLick on "Login" button
|
|
19
|
+
- click on input "username"
|
|
20
|
+
- fill username as "foo@bar.com"
|
|
21
|
+
- fill password as "bar@123"
|
|
22
|
+
- click on "Login Button"
|
|
23
|
+
assert: "Login successful" text should be visible # assertion
|
|
24
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bin/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const openai_1 = __importDefault(require("openai"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
// const { scenarios } = require("./scenarios");
|
|
11
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
12
|
+
const prettier_1 = __importDefault(require("prettier"));
|
|
13
|
+
const eslint_1 = require("eslint");
|
|
14
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
15
|
+
const yaml_1 = require("yaml");
|
|
16
|
+
dotenv_1.default.config({
|
|
17
|
+
path: [".env.local", ".env"],
|
|
18
|
+
});
|
|
19
|
+
async function readFilesInDirectory(dir = "") {
|
|
20
|
+
let files = [];
|
|
21
|
+
const items = await fs_extra_1.default.readdir(dir);
|
|
22
|
+
for (const item of items) {
|
|
23
|
+
const fullPath = path_1.default.join(dir, item);
|
|
24
|
+
const stat = await fs_extra_1.default.stat(fullPath);
|
|
25
|
+
if (stat.isDirectory()) {
|
|
26
|
+
files = files.concat(await readFilesInDirectory(fullPath));
|
|
27
|
+
}
|
|
28
|
+
else if (stat.isFile()) {
|
|
29
|
+
const content = await fs_extra_1.default.readFile(fullPath, "utf-8");
|
|
30
|
+
files.push({ filePath: fullPath, content });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return files;
|
|
34
|
+
}
|
|
35
|
+
function mergeFilesToPrompt(files = []) {
|
|
36
|
+
let prompt = "";
|
|
37
|
+
files.forEach((file) => {
|
|
38
|
+
prompt += `File Path: ${file.filePath}\n`;
|
|
39
|
+
prompt += `File:\n`;
|
|
40
|
+
prompt += `${file.content}\n\n ------ \n\n`;
|
|
41
|
+
});
|
|
42
|
+
return prompt;
|
|
43
|
+
}
|
|
44
|
+
async function generatePromptFromDirectory(dir = "") {
|
|
45
|
+
try {
|
|
46
|
+
const files = await readFilesInDirectory(dir);
|
|
47
|
+
const prompt = mergeFilesToPrompt(files);
|
|
48
|
+
return prompt;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error("Error reading directory:", error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function getLLMResult(instruction) {
|
|
55
|
+
const openai = new openai_1.default();
|
|
56
|
+
const completion = await openai.chat.completions.create({
|
|
57
|
+
messages: [
|
|
58
|
+
{
|
|
59
|
+
role: "user",
|
|
60
|
+
content: [
|
|
61
|
+
{
|
|
62
|
+
type: "text",
|
|
63
|
+
text: instruction,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
model: "gpt-4o",
|
|
69
|
+
});
|
|
70
|
+
console.log("generated completion");
|
|
71
|
+
const response = completion.choices[0]?.message.content || "";
|
|
72
|
+
return response;
|
|
73
|
+
}
|
|
74
|
+
function validateTypescript(filePath) {
|
|
75
|
+
// Create a compiler host to read files
|
|
76
|
+
const compilerHost = typescript_1.default.createCompilerHost({});
|
|
77
|
+
compilerHost.readFile = (file) => fs_extra_1.default.readFileSync(file, "utf8");
|
|
78
|
+
// Create a program with a single source file
|
|
79
|
+
const program = typescript_1.default.createProgram([filePath], {}, compilerHost);
|
|
80
|
+
// Get the source file
|
|
81
|
+
const sourceFile = program.getSourceFile(filePath);
|
|
82
|
+
// Get and report any syntactic errors
|
|
83
|
+
const errors = [];
|
|
84
|
+
const syntacticDiagnostics = program.getSyntacticDiagnostics(sourceFile);
|
|
85
|
+
if (syntacticDiagnostics.length > 0) {
|
|
86
|
+
console.log("Syntactic errors:");
|
|
87
|
+
syntacticDiagnostics.forEach((diagnostic) => {
|
|
88
|
+
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
|
|
89
|
+
const message = typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
|
|
90
|
+
console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
|
|
91
|
+
if (typeof diagnostic.messageText === "string") {
|
|
92
|
+
errors.push(diagnostic.messageText);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.log("No syntactic errors.");
|
|
98
|
+
}
|
|
99
|
+
// Get and report any semantic errors
|
|
100
|
+
const semanticDiagnostics = program.getSemanticDiagnostics(sourceFile);
|
|
101
|
+
if (semanticDiagnostics.length > 0) {
|
|
102
|
+
console.log("Semantic errors:");
|
|
103
|
+
semanticDiagnostics.forEach((diagnostic) => {
|
|
104
|
+
console.log(diagnostic.messageText);
|
|
105
|
+
if (typeof diagnostic.messageText == "string") {
|
|
106
|
+
errors.push(diagnostic.messageText);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.log("No semantic errors.");
|
|
112
|
+
}
|
|
113
|
+
return errors;
|
|
114
|
+
}
|
|
115
|
+
async function formatCode(filePath) {
|
|
116
|
+
const fileContent = fs_extra_1.default.readFileSync(filePath, "utf8");
|
|
117
|
+
const prettierConfig = {};
|
|
118
|
+
const formattedContent = await prettier_1.default.format(fileContent, {
|
|
119
|
+
...prettierConfig,
|
|
120
|
+
filepath: filePath,
|
|
121
|
+
});
|
|
122
|
+
fs_extra_1.default.writeFileSync(filePath, formattedContent);
|
|
123
|
+
console.log("File formatted successfully.");
|
|
124
|
+
}
|
|
125
|
+
async function stripAndPrependImports(content) {
|
|
126
|
+
const imports = content.match(/import.* from.*;/g);
|
|
127
|
+
const strippedContent = content.replace(/import.* from.*;/g, "");
|
|
128
|
+
const prependContent = (imports?.join("\n") || "") + "\n\n";
|
|
129
|
+
return [prependContent, strippedContent];
|
|
130
|
+
}
|
|
131
|
+
async function lintErrors(filePath) {
|
|
132
|
+
const eslint = new eslint_1.ESLint({
|
|
133
|
+
fix: true,
|
|
134
|
+
useEslintrc: true,
|
|
135
|
+
});
|
|
136
|
+
const [result] = await eslint.lintFiles(filePath);
|
|
137
|
+
fs_extra_1.default.writeFileSync(filePath, result?.output || "");
|
|
138
|
+
}
|
|
139
|
+
async function generateTest(scenarios, file) {
|
|
140
|
+
const codePrompt = await generatePromptFromDirectory("./tests");
|
|
141
|
+
const pomPrompt = await generatePromptFromDirectory("./pages");
|
|
142
|
+
const testFileContent = fs_extra_1.default.readFileSync(file, "utf-8");
|
|
143
|
+
for (const i in scenarios) {
|
|
144
|
+
const scenario = scenarios[i];
|
|
145
|
+
console.log("Generating test for scenario: ", scenario?.name);
|
|
146
|
+
//TODO: improve this logic. its buggy
|
|
147
|
+
if (testFileContent.includes(`test("${scenario?.name}"`)) {
|
|
148
|
+
console.log("test already exists for this scenario", scenario?.name);
|
|
149
|
+
console.log("skipping test generation");
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const instruction = `
|
|
153
|
+
You are a software test engineer who is given a task to create test basis a scenario provided to you.
|
|
154
|
+
You will be provided with current tests, fixtures and page object models for you to use and write test.
|
|
155
|
+
You need to respond with only with the test case in typescript language using playwright.
|
|
156
|
+
|
|
157
|
+
Here is the list of current tests and fixtures:
|
|
158
|
+
|
|
159
|
+
${codePrompt}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
Here is the list of current page object models:
|
|
163
|
+
|
|
164
|
+
${pomPrompt}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
Following is the test scenario for which you need to write test:
|
|
168
|
+
name: ${scenario?.name}
|
|
169
|
+
Steps:
|
|
170
|
+
${scenario?.steps?.join("\n")}
|
|
171
|
+
|
|
172
|
+
Assert:
|
|
173
|
+
${scenario?.assert}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
The code needs to be written inside ${file} file. Write the tests accordingly.
|
|
177
|
+
|
|
178
|
+
Follow these guidelines before responding with output
|
|
179
|
+
- Ensure there are no type issues in the code generated
|
|
180
|
+
- For the given spec file respond with only the test case for the given scenario
|
|
181
|
+
- Donot respond with markdown syntax or backticks
|
|
182
|
+
- Respond only with the code
|
|
183
|
+
- Donot repeat steps which are already mentioned in the "test.beforeEach" block
|
|
184
|
+
- Add import statements at the beginning of the output.
|
|
185
|
+
`;
|
|
186
|
+
console.log("constructed instruction for llm");
|
|
187
|
+
let response = await getLLMResult(instruction);
|
|
188
|
+
const contents = fs_extra_1.default.readFileSync(file, "utf-8");
|
|
189
|
+
const [prependContent, strippedContent] = await stripAndPrependImports(response);
|
|
190
|
+
await fs_extra_1.default.writeFile(file, prependContent + contents + `\n\n${strippedContent}`, "utf-8");
|
|
191
|
+
console.log("Linting generated output");
|
|
192
|
+
await lintErrors(file);
|
|
193
|
+
console.log("Validating types");
|
|
194
|
+
let errors = validateTypescript(file);
|
|
195
|
+
const maxIteration = 2;
|
|
196
|
+
let counter = 0;
|
|
197
|
+
while (errors.length > 0) {
|
|
198
|
+
console.log(errors);
|
|
199
|
+
const fileContent = fs_extra_1.default.readFileSync(file, "utf-8");
|
|
200
|
+
counter += 1;
|
|
201
|
+
if (counter > maxIteration) {
|
|
202
|
+
console.log(`Max iteration limit reached. Please review the file ${file} and manually fix the typescript errors.`);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
const instruction = `
|
|
206
|
+
You are a software engineer who is given a task to create test basis a scenario provided to you.
|
|
207
|
+
You will be provided with current tests, fixtures and page object models for you to use and write test.
|
|
208
|
+
There are following typescript errors in ${file} which you need to fix.
|
|
209
|
+
|
|
210
|
+
Here is the list of current tests and fixtures:
|
|
211
|
+
|
|
212
|
+
${codePrompt}
|
|
213
|
+
|
|
214
|
+
Here is the list of current page object models:
|
|
215
|
+
|
|
216
|
+
${pomPrompt}
|
|
217
|
+
|
|
218
|
+
Here are the typescript errors:
|
|
219
|
+
${errors.join("\n")}
|
|
220
|
+
|
|
221
|
+
Here is the content of the file ${file}:
|
|
222
|
+
|
|
223
|
+
${fileContent}
|
|
224
|
+
|
|
225
|
+
Follow following guidelines before responding with output
|
|
226
|
+
- Ensure there are no type issues in the file ${file}
|
|
227
|
+
- For the given file respond with only the code
|
|
228
|
+
- Donot respond with markdown syntax or backticks
|
|
229
|
+
- Respond only with the code
|
|
230
|
+
- Donot modify anything else apart from the code required to fix typescript error
|
|
231
|
+
`;
|
|
232
|
+
response = await getLLMResult(instruction);
|
|
233
|
+
console.log("fixed");
|
|
234
|
+
console.log(response);
|
|
235
|
+
errors = validateTypescript(file);
|
|
236
|
+
await fs_extra_1.default.writeFile(file, response, "utf-8");
|
|
237
|
+
}
|
|
238
|
+
console.log("formatting spec file");
|
|
239
|
+
await formatCode(file);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
(async function main() {
|
|
243
|
+
if (process.argv.length != 3) {
|
|
244
|
+
console.log("Provide a path to scenarios path to generate test");
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
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);
|
|
261
|
+
}
|
|
262
|
+
await generateTest(scenarios, filePath);
|
|
263
|
+
}
|
|
264
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@empiricalrun/test-gen",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"registry": "https://registry.npmjs.org/",
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"bin": {
|
|
9
|
+
"@empiricalrun/test-gen": "dist/bin/index.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/empirical-run/empirical.git"
|
|
15
|
+
},
|
|
16
|
+
"author": "Empirical Team <hey@empirical.run>",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"dotenv": "^16.4.5",
|
|
19
|
+
"fs-extra": "^11.2.0",
|
|
20
|
+
"openai": "^4.47.2",
|
|
21
|
+
"yaml": "^2.4.2"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/fs-extra": "^11.0.4"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "tsc --build --watch",
|
|
28
|
+
"build": "tsc --build",
|
|
29
|
+
"clean": "tsc --build --clean",
|
|
30
|
+
"lint": "eslint .",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest"
|
|
33
|
+
}
|
|
34
|
+
}
|