@competent-devs/test-forge
Version:
Package for UI unit test generation based on storybook context
87 lines (85 loc) • 3.54 kB
JavaScript
import * as path from "node:path";
import * as fs from "node:fs/promises";
import { globbyStream } from "globby";
import Mustache from "mustache";
import * as prettier from "prettier";
import { extractSource, readCsf } from "storybook/internal/csf-tools";
import { loadMainConfig } from "storybook/internal/common";
import { HumanMessage } from "@langchain/core/messages";
import { invokeLlm } from "./agent.js";
const ROOT_DIR = path.resolve("./");
const STORYBOOK_CONFIG_DIR = path.resolve(ROOT_DIR, "./.storybook");
const EXTENSION_MDX = ".mdx";
export const getPromptTemplate = async () => {
return await fs.readFile(path.resolve("./src/test-forge/prompt.mustache"), "utf8");
};
export const getStorybookConfig = async () => {
return await loadMainConfig({
configDir: STORYBOOK_CONFIG_DIR,
});
};
export const testForge = async () => {
const storybookConfig = await getStorybookConfig();
for await (const filePath of globbyStream(storybookConfig.stories, {
onlyFiles: true,
cwd: STORYBOOK_CONFIG_DIR,
})) {
if (isMDX(filePath)) {
continue;
}
console.group(`===== ${filePath} =====`);
const relativepathtoroot = path.relative(STORYBOOK_CONFIG_DIR, ROOT_DIR);
const pathtostoryfromroot = path.relative(relativepathtoroot, filePath);
const parsedpath = path.parse(pathtostoryfromroot);
const csffile = await readCsfFile(pathtostoryfromroot);
const testcode = await generateTests(pathtostoryfromroot, csffile);
const testfilename = path.resolve(path.dirname(pathtostoryfromroot), `${parsedpath.name}.test.tsx`);
await writeTestFile(testfilename, testcode);
console.groupEnd();
}
};
function isMDX(filePath) {
return path.extname(filePath) === EXTENSION_MDX;
}
async function readCsfFile(filePath) {
console.log(`Reading CSF file ${filePath}`);
return (await readCsf(filePath, {
makeTitle: (userTitle) => userTitle || "default",
})).parse();
}
async function generateTests(filePath, csfFile) {
const parsedFilePath = path.parse(filePath);
let code = `
import { test, expect } from 'vitest'
import { screen, within } from '@testing-library/react'
import { composeStories } from '@storybook/react'
import * as stories from './${parsedFilePath.name}'
const { ${csfFile.indexInputs.map((indexInput) => indexInput.exportName).join(", ")} } = composeStories(stories)
`;
for (const indexInput of csfFile.indexInputs) {
if (indexInput.type !== "story") {
continue;
}
code += `
test('${indexInput.name}', async () => {
await ${indexInput.exportName}.run()
`;
code = await autocompleteTest(code, indexInput, csfFile);
}
return prettier.format(code, { parser: "typescript" });
}
async function autocompleteTest(code, indexInput, csfFile) {
const storyExportName = indexInput.exportName;
const storySource = extractSource(csfFile.getStoryExport(storyExportName));
const PROMPT_TEMPLATE = await getPromptTemplate();
const messages = [];
messages.push(new HumanMessage(Mustache.render(PROMPT_TEMPLATE, { code, storyExportName, storySource })));
const response = await invokeLlm(messages);
code += response?.content || "\n})" + "\n";
return code;
}
async function writeTestFile(fileName, code) {
console.log("Writing test file", fileName);
return fs.writeFile(fileName, code, { flag: "w+" });
}
//# sourceMappingURL=test-forge.js.map