@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 ADDED
@@ -0,0 +1,13 @@
1
+ # @empiricalrun/test-gen
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - c75a102: chore: add publish package support
8
+
9
+ ## 0.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 20c6004: feat: add support for package publish
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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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
+ }