├── .husky └── pre-commit ├── postcss.config.js ├── .env.example ├── .prettierrc ├── next.config.js ├── .prettierignore ├── tailwind.config.ts ├── lib ├── questions.ts ├── testTypes.ts ├── genericQuestions.json ├── testCaseUtils.ts ├── settings.ts └── autoContext.ts ├── tsconfig.node.json ├── .eslintrc.json ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── app ├── globals.css ├── api │ ├── classify │ │ └── route.ts │ ├── generate │ │ └── route.ts │ └── generate-questions │ │ └── route.ts ├── layout.tsx ├── settings │ └── page.tsx ├── questions │ └── page.tsx ├── page.tsx └── results │ └── page.tsx ├── tsconfig.json ├── package.json ├── components ├── SettingsProvider.tsx ├── ToastProvider.tsx ├── ConfirmDialogProvider.tsx ├── SavedTestsList.tsx └── BoilerplateModal.tsx ├── README.md └── contributing.md /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | npm run format:check 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | # Get your key at: https://platform.openai.com/account/api-keys 3 | OPENAI_API_KEY=sk_your_api_key_here -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "trailingComma": "es5", 5 | "printWidth": 100, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | out 4 | coverage 5 | dist 6 | build 7 | 8 | # lockfiles generated by package managers shouldn't be formatted 9 | package-lock.json 10 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: ["./app/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | darkMode: "class", 10 | }; 11 | export default config; 12 | -------------------------------------------------------------------------------- /lib/questions.ts: -------------------------------------------------------------------------------- 1 | export const categoryDescriptions: Record = { 2 | "backend-api": "REST API Endpoint", 3 | "frontend-component": "Frontend Component", 4 | database: "Database Operation", 5 | "library-function": "Library Function", 6 | integration: "System Integration", 7 | "data-pipeline": "Data Pipeline", 8 | other: "Other", 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "lib": ["ES2020"], 8 | "module": "ESNext", 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true 11 | }, 12 | "include": ["next.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /lib/testTypes.ts: -------------------------------------------------------------------------------- 1 | export enum TestType { 2 | Unit = "unit", 3 | Integration = "integration", 4 | Feature = "feature", 5 | Performance = "performance", 6 | Manual = "manual", 7 | } 8 | 9 | export const TEST_TYPE_VALUES = [ 10 | TestType.Unit, 11 | TestType.Integration, 12 | TestType.Feature, 13 | TestType.Performance, 14 | TestType.Manual, 15 | ] as const; 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "prettier"], 3 | "plugins": ["@typescript-eslint"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "project": "./tsconfig.json" 7 | }, 8 | "rules": { 9 | "no-unused-vars": "off", 10 | "@typescript-eslint/no-unused-vars": [ 11 | "error", 12 | { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/genericQuestions.json: -------------------------------------------------------------------------------- 1 | [ 2 | "What is the main purpose of this code?", 3 | "What are the primary inputs?", 4 | "What are the expected outputs?", 5 | "What error cases should be tested?", 6 | "What are the edge cases?", 7 | "What performance requirements exist?", 8 | "What security considerations matter?", 9 | "What dependencies are involved?", 10 | "What validation rules apply?", 11 | "What should not be tested?" 12 | ] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # next.js 10 | /.next/ 11 | /out/ 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env.local 27 | .env.*.local 28 | 29 | # vercel 30 | .vercel 31 | 32 | # typescript 33 | *.tsbuildinfo 34 | next-env.d.ts 35 | .idea -------------------------------------------------------------------------------- /lib/testCaseUtils.ts: -------------------------------------------------------------------------------- 1 | import type { Settings } from "@/lib/settings"; 2 | 3 | interface TestCaseLike { 4 | testType: string; 5 | [key: string]: any; 6 | } 7 | 8 | export function filterTestCasesBySettings( 9 | testCases: T[], 10 | settings: Settings 11 | ): T[] { 12 | const disabledTypes = Object.entries(settings.disabledTestTypes) 13 | .filter(([, disabled]) => disabled) 14 | .map(([type]) => type); 15 | 16 | if (!disabledTypes.length) { 17 | return testCases; 18 | } 19 | 20 | return testCases.filter((testCase) => !disabledTypes.includes(testCase.testType)); 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Format 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | quality-checks: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 18 20 | cache: npm 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Run linting 26 | run: npm run lint 27 | 28 | - name: Run formatting check 29 | run: npm run format:check 30 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | } 10 | 11 | html { 12 | scroll-behavior: smooth; 13 | } 14 | 15 | body { 16 | font-family: 17 | -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", 18 | "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | background-color: white; 22 | color: #000; 23 | transition: 24 | background-color 0.3s, 25 | color 0.3s; 26 | } 27 | 28 | body.dark { 29 | background-color: #0f172a; 30 | color: #f1f5f9; 31 | } 32 | 33 | * { 34 | @apply transition-colors duration-300; 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["es2020", "dom", "dom.iterable"], 6 | "module": "esnext", 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "isolatedModules": true, 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "jsx": "preserve", 17 | "paths": { 18 | "@/*": ["./*"] 19 | }, 20 | "allowJs": true, 21 | "incremental": true, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ] 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assertify", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "prepare": "husky install", 11 | "format": "prettier --write .", 12 | "format:check": "prettier --check ." 13 | }, 14 | "dependencies": { 15 | "next": "^14.1.0", 16 | "openai": "^4.28.0", 17 | "react": "^18.3.1", 18 | "react-dom": "^18.3.1" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^20.10.6", 22 | "@types/react": "^18.2.47", 23 | "@types/react-dom": "^18.2.18", 24 | "@typescript-eslint/eslint-plugin": "^8.48.0", 25 | "@typescript-eslint/parser": "^8.48.0", 26 | "autoprefixer": "^10.4.16", 27 | "eslint": "^8.57.0", 28 | "eslint-config-next": "^14.2.18", 29 | "eslint-config-prettier": "^9.1.0", 30 | "husky": "^9.1.7", 31 | "postcss": "^8.4.32", 32 | "prettier": "^3.6.2", 33 | "tailwindcss": "^3.4.1", 34 | "typescript": "^5.3.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/api/classify/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { OpenAI } from "openai"; 3 | 4 | export async function POST(req: NextRequest) { 5 | try { 6 | const { projectDescription, apiKey } = await req.json(); 7 | 8 | if (!projectDescription) { 9 | return NextResponse.json({ error: "Project description required" }, { status: 400 }); 10 | } 11 | 12 | if (!apiKey) { 13 | return NextResponse.json({ error: "API key required" }, { status: 400 }); 14 | } 15 | 16 | const openai = new OpenAI({ 17 | apiKey: apiKey, 18 | }); 19 | 20 | const categories = [ 21 | "backend-api", 22 | "frontend-component", 23 | "database", 24 | "library-function", 25 | "integration", 26 | "data-pipeline", 27 | ]; 28 | 29 | const message = await openai.chat.completions.create({ 30 | model: "gpt-4", 31 | messages: [ 32 | { 33 | role: "system", 34 | content: `You are a project classifier. Classify the given project description into one of these categories: ${categories.join( 35 | ", " 36 | )}. Respond with ONLY the category name, nothing else.`, 37 | }, 38 | { 39 | role: "user", 40 | content: projectDescription, 41 | }, 42 | ], 43 | }); 44 | 45 | let category = (message.choices[0].message.content || "other").trim().toLowerCase(); 46 | 47 | if (!categories.includes(category)) { 48 | category = "other"; 49 | } 50 | 51 | return NextResponse.json({ category }); 52 | } catch (error: any) { 53 | console.error("Classification error:", error); 54 | 55 | if (error.status === 401) { 56 | return NextResponse.json({ error: "Unauthorized: Invalid API key" }, { status: 401 }); 57 | } 58 | 59 | return NextResponse.json( 60 | { error: "Classification failed", details: String(error.message) }, 61 | { status: 500 } 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /components/SettingsProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | createContext, 5 | useCallback, 6 | useContext, 7 | useEffect, 8 | useMemo, 9 | useState, 10 | type PropsWithChildren, 11 | } from "react"; 12 | import { SETTINGS_STORAGE_KEY, Settings, sanitizeSettings, defaultSettings } from "@/lib/settings"; 13 | 14 | interface SettingsContextValue { 15 | settings: Settings; 16 | updateSettings: (updates: Partial) => void; 17 | } 18 | 19 | const SettingsContext = createContext(undefined); 20 | 21 | export function SettingsProvider({ children }: PropsWithChildren) { 22 | const [settings, setSettings] = useState(defaultSettings); 23 | 24 | useEffect(() => { 25 | try { 26 | const stored = localStorage.getItem(SETTINGS_STORAGE_KEY); 27 | if (stored) { 28 | const parsed = JSON.parse(stored); 29 | setSettings(sanitizeSettings(parsed)); 30 | } 31 | } catch (error) { 32 | console.error("Failed to load settings: ", error); 33 | } 34 | }, []); 35 | 36 | const persist = useCallback((value: Settings) => { 37 | try { 38 | localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(value)); 39 | } catch (error) { 40 | console.error("Failed to persist settings:", error); 41 | } 42 | }, []); 43 | 44 | const updateSettings = useCallback( 45 | (updates: Partial) => { 46 | setSettings((prev) => { 47 | const merged = sanitizeSettings({ ...prev, ...updates }); 48 | persist(merged); 49 | return merged; 50 | }); 51 | }, 52 | [persist] 53 | ); 54 | 55 | const value = useMemo(() => ({ settings, updateSettings }), [settings, updateSettings]); 56 | 57 | return {children}; 58 | } 59 | 60 | export function useSettings(): SettingsContextValue { 61 | const context = useContext(SettingsContext); 62 | if (!context) { 63 | throw new Error("useSettings must be used within a SettingsProvider"); 64 | } 65 | return context; 66 | } 67 | -------------------------------------------------------------------------------- /lib/settings.ts: -------------------------------------------------------------------------------- 1 | import { TestType, TEST_TYPE_VALUES } from "./testTypes"; 2 | 3 | export { TestType } from "./testTypes"; 4 | 5 | export const boilerplateOptions = [ 6 | "vitest", 7 | "jest", 8 | "pytest", 9 | "unittest", 10 | "junit", 11 | "phpunit", 12 | "rspec", 13 | "mocha", 14 | ] as const; 15 | 16 | export type BoilerplateKey = (typeof boilerplateOptions)[number]; 17 | 18 | export interface Settings { 19 | defaultContext: string; 20 | disabledTestTypes: Record; 21 | boilerplateSampleSize: number; 22 | disabledBoilerplates: Record; 23 | } 24 | 25 | export const SETTINGS_STORAGE_KEY = "tcg_settings"; 26 | 27 | export const testTypeOptions: TestType[] = [...TEST_TYPE_VALUES]; 28 | 29 | export const defaultSettings: Settings = { 30 | defaultContext: "", 31 | disabledTestTypes: { 32 | [TestType.Unit]: false, 33 | [TestType.Integration]: false, 34 | [TestType.Feature]: false, 35 | [TestType.Performance]: false, 36 | [TestType.Manual]: false, 37 | }, 38 | boilerplateSampleSize: 5, 39 | disabledBoilerplates: boilerplateOptions.reduce( 40 | (acc, key) => { 41 | acc[key] = false; 42 | return acc; 43 | }, 44 | {} as Record 45 | ), 46 | }; 47 | 48 | export function sanitizeSettings(partial?: Partial): Settings { 49 | const merged: Settings = { 50 | defaultContext: partial?.defaultContext ?? defaultSettings.defaultContext, 51 | boilerplateSampleSize: 52 | typeof partial?.boilerplateSampleSize === "number" 53 | ? partial.boilerplateSampleSize 54 | : defaultSettings.boilerplateSampleSize, 55 | disabledTestTypes: { 56 | ...defaultSettings.disabledTestTypes, 57 | ...(partial?.disabledTestTypes ?? {}), 58 | }, 59 | disabledBoilerplates: { 60 | ...defaultSettings.disabledBoilerplates, 61 | ...(partial?.disabledBoilerplates ?? {}), 62 | }, 63 | }; 64 | 65 | merged.boilerplateSampleSize = Math.min(5, Math.max(1, Math.round(merged.boilerplateSampleSize))); 66 | 67 | return merged; 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Assertify - AI-Powered Test Case Generation 2 | 3 | Assertify is an intelligent testing assistant that analyzes your project description, asks clarifying questions, and generates comprehensive test suites across multiple testing styles and frameworks. 4 | 5 | ## Features 6 | 7 | - **AI project analysis** that classifies your work and requests extra context before any generation starts. 8 | - **Dynamic question generation** with a resilient fallback list so you never get stuck when the API is unavailable. 9 | - **Configurable settings** for persistent default context, disabling unwanted test types, and tailoring boilerplate sample sizes. 10 | - **Comprehensive test coverage** spanning unit, integration, feature, performance, and manual scenarios with prioritization metadata. 11 | - **Boilerplate code generation** for eight popular frameworks, respecting the frameworks you disable in settings. 12 | - **Flexible exports and saving** allowing JSON/CSV downloads plus local storage history. 13 | - **Responsive, themable UI** that works on desktop, tablet, and mobile with light/dark theme support. 14 | 15 | ## Supported Testing Frameworks 16 | 17 | - Vitest (JavaScript/TypeScript) 18 | - Jest (JavaScript/TypeScript) 19 | - Pytest (Python) 20 | - Unittest (Python) 21 | - JUnit (Java) 22 | - PHPUnit (PHP) 23 | - RSpec (Ruby) 24 | - Mocha (JavaScript) 25 | 26 | ## Tech Stack 27 | 28 | - Next.js 14 29 | - TypeScript 30 | - Tailwind CSS 31 | - OpenAI API 32 | 33 | ## Installation 34 | 35 | 1. Install dependencies: `npm install` 36 | 2. Copy the environment file: `cp .env.example .env.local` 37 | 3. Add your OpenAI API key to `.env.local` 38 | 4. Start the dev server: `npm run dev` 39 | 5. Open http://localhost:3000 in your browser 40 | 41 | ## How to Use 42 | 43 | 1. Provide your project description from the landing page; the app automatically classifies the category. 44 | 2. Answer the context questions (or skip) so the generator can tailor scenarios to your needs. 45 | 3. Review generated tests on the results page, filter by type or priority, and inspect the suggested testing strategy and risk areas. 46 | 4. Generate boilerplate code for your preferred frameworks or export the dataset as JSON/CSV. 47 | 5. Manage settings at `/settings` to define default context, disable frameworks, and control boilerplate sample sizes. 48 | 49 | ## Future Improvements 50 | 51 | - Allow selecting different LLM providers and models per generation so teams can optimize for latency or cost. 52 | - Offer more granular configuration for question generation (e.g., required question count, tone, or domain presets). 53 | - Optimize large test suites by streaming responses and deduplicating similar scenarios before persistence. 54 | - Provide deeper integrations with CI/CD by exporting ready-to-run suites or syncing with test management tools. 55 | 56 | ## License 57 | 58 | MIT 59 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Assertify 2 | 3 | Thanks for considering a contribution to Assertify! This document outlines how to set up a local environment, the coding standards we follow, and what we expect from every pull request. 4 | 5 | ## Ways to Contribute 6 | - Report bugs or request features by opening an Issue with clear reproduction steps or user stories. 7 | - Improve documentation, copy, or accessibility anywhere in the repo. 8 | - Tackle open Issues that are tagged `good first issue`, `help wanted`, or any topic you feel comfortable owning. 9 | 10 | ## Prerequisites 11 | - **Node.js 18.x** and the matching npm release (the CI pipeline pins Node 18). 12 | - An OpenAI API key stored in `.env.local` so you can exercise AI-powered features locally. 13 | - `git` and GitHub access so you can fork and open pull requests. 14 | 15 | ## Local Development 16 | 1. Fork the repository on GitHub and clone your fork. 17 | 2. Install dependencies: `npm install`. 18 | 3. Copy the example environment file: `cp .env.example .env.local`, then add your OpenAI API key. 19 | 4. Launch the dev server with `npm run dev` and visit `http://localhost:3000`. 20 | 5. Run `npm run build` before opening a pull request to ensure Next.js can compile. 21 | 22 | ## Branching, Commits, and Style 23 | - Create feature branches off `master` using the pattern `type/short-description` (e.g., `feat/context-questions`). 24 | - Keep commits focused; prefer smaller commits that describe *why* a change exists. 25 | - Follow the existing TypeScript, React, and Tailwind conventions already present in the code you touch. 26 | 27 | ## Quality Checks 28 | The GitHub Actions workflow (`.github/workflows/ci.yml`) runs linting and formatting on every pull request. Run the same commands locally before pushing: 29 | 30 | ```bash 31 | npm run lint 32 | npm run format:check 33 | ``` 34 | 35 | Use `npm run format` to auto-format files. Please fix all warnings or explain why they cannot be resolved in the pull request description. 36 | 37 | ## Pull Request Checklist 38 | - [ ] Explain the motivation and link the Issue being addressed. 39 | - [ ] Include screenshots or recordings for UI changes (desktop + mobile where relevant). 40 | - [ ] Update documentation (README, docs, or component stories) when behavior changes. 41 | - [ ] Ensure `npm run build`, `npm run lint`, and `npm run format:check` succeed locally. 42 | - [ ] Request at least one review and be responsive to feedback. 43 | 44 | ## Reporting Issues 45 | When filing an Issue, include: 46 | - Expected vs. actual behavior and reproduction steps. 47 | - Environment details (OS, browser, Node version) if applicable. 48 | - Logs, stack traces, or screenshots that make the problem easier to diagnose. 49 | 50 | ## Questions 51 | Unsure about the best approach or need clarification before opening a PR? Start a GitHub Discussion or comment on the related Issue so we can align on a solution before you invest significant time. 52 | 53 | Happy testing! 54 | -------------------------------------------------------------------------------- /app/api/generate/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { OpenAI } from "openai"; 3 | 4 | export async function POST(req: NextRequest) { 5 | try { 6 | const { projectDescription, category, answers, apiKey } = await req.json(); 7 | 8 | if (!projectDescription || !category || !answers) { 9 | return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); 10 | } 11 | 12 | if (!apiKey) { 13 | return NextResponse.json({ error: "API key required" }, { status: 400 }); 14 | } 15 | 16 | const openai = new OpenAI({ 17 | apiKey: apiKey, 18 | }); 19 | 20 | const prompt = `You are a QA expert. Generate comprehensive test cases for the following project: 21 | 22 | Category: ${category} 23 | Description: ${projectDescription} 24 | 25 | Context from developer: 26 | ${answers.map((a: string, i: number) => `${i + 1}. ${a}`).join("\n")} 27 | 28 | Generate test cases in JSON format with this structure: 29 | { 30 | "testCases": [ 31 | { 32 | "testType": "unit|integration|feature|performance|manual", 33 | "testName": "test name", 34 | "description": "what is being tested", 35 | "givenContext": "preconditions", 36 | "testSteps": ["step 1", "step 2"], 37 | "expectedOutcome": "what should happen", 38 | "priority": "high|medium|low", 39 | "notes": "additional notes" 40 | } 41 | ], 42 | "testingStrategy": "overall strategy", 43 | "riskAreas": ["risk 1", "risk 2"] 44 | } 45 | 46 | Generate 12-15 diverse test cases covering: 47 | - Unit tests (3-4) 48 | - Integration tests (2-3) 49 | - Feature/acceptance tests (3-4) 50 | - Performance tests (1-2) 51 | - Manual tests (2-3) 52 | 53 | Respond ONLY with valid JSON, no markdown or extra text.`; 54 | 55 | const message = await openai.chat.completions.create({ 56 | model: "gpt-4", 57 | messages: [ 58 | { 59 | role: "system", 60 | content: 61 | "You are a QA expert that generates test cases. Always respond with valid JSON only.", 62 | }, 63 | { 64 | role: "user", 65 | content: prompt, 66 | }, 67 | ], 68 | temperature: 0.7, 69 | }); 70 | 71 | const content = message.choices[0].message.content || "{}"; 72 | 73 | let parsedResponse; 74 | try { 75 | parsedResponse = JSON.parse(content); 76 | } catch { 77 | const jsonMatch = content.match(/\{[\s\S]*\}/); 78 | if (jsonMatch) { 79 | parsedResponse = JSON.parse(jsonMatch[0]); 80 | } else { 81 | throw new Error("Invalid JSON response"); 82 | } 83 | } 84 | 85 | return NextResponse.json(parsedResponse); 86 | } catch (error: any) { 87 | console.error("Generation error:", error); 88 | 89 | if (error.status === 401) { 90 | return NextResponse.json({ error: "Unauthorized: Invalid API key" }, { status: 401 }); 91 | } 92 | 93 | return NextResponse.json( 94 | { error: "Test case generation failed", details: String(error.message) }, 95 | { status: 500 } 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/api/generate-questions/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { OpenAI } from "openai"; 3 | 4 | export async function POST(req: NextRequest) { 5 | try { 6 | const { projectDescription, category, apiKey } = await req.json(); 7 | 8 | console.log("Received:", { 9 | projectDescription: !!projectDescription, 10 | category, 11 | apiKey: !!apiKey, 12 | }); 13 | 14 | if (!projectDescription || !category) { 15 | return NextResponse.json( 16 | { error: "Missing projectDescription or category" }, 17 | { status: 400 } 18 | ); 19 | } 20 | 21 | if (!apiKey) { 22 | return NextResponse.json({ error: "API key required" }, { status: 400 }); 23 | } 24 | 25 | const openai = new OpenAI({ 26 | apiKey: apiKey, 27 | }); 28 | 29 | const prompt = `You are a QA expert. Based on the following project description and category, generate exactly 10 specific, actionable questions that a developer should answer to help write comprehensive tests for this project. 30 | 31 | Project Category: ${category} 32 | Project Description: ${projectDescription} 33 | 34 | Generate 10 questions that are: 35 | - Specific to this project type 36 | - Actionable and clear 37 | - Focused on testing requirements 38 | - Cover different aspects (inputs, outputs, errors, edge cases, performance, security, etc.) 39 | 40 | Format your response as a JSON array of strings, like this: 41 | ["Question 1?", "Question 2?", "Question 3?", ...] 42 | 43 | Respond ONLY with the JSON array, no additional text or markdown.`; 44 | 45 | const message = await openai.chat.completions.create({ 46 | model: "gpt-4", 47 | messages: [ 48 | { 49 | role: "system", 50 | content: 51 | "You are a QA expert. Generate exactly 10 testing-focused questions. Respond ONLY with a JSON array of strings.", 52 | }, 53 | { 54 | role: "user", 55 | content: prompt, 56 | }, 57 | ], 58 | temperature: 0.7, 59 | }); 60 | 61 | const content = message.choices[0].message.content || "[]"; 62 | 63 | let questions; 64 | try { 65 | questions = JSON.parse(content); 66 | if (!Array.isArray(questions)) { 67 | throw new Error("Response is not an array"); 68 | } 69 | questions = questions.slice(0, 10); 70 | } catch (parseError) { 71 | console.error("Failed to parse questions:", content, parseError); 72 | questions = [ 73 | "What is the main purpose of this code?", 74 | "What are the primary inputs?", 75 | "What are the expected outputs?", 76 | "What error cases should be tested?", 77 | "What are the edge cases?", 78 | "What performance requirements exist?", 79 | "What security considerations matter?", 80 | "What dependencies are involved?", 81 | "What validation rules apply?", 82 | "What should not be tested?", 83 | ]; 84 | } 85 | 86 | return NextResponse.json({ questions }); 87 | } catch (error: any) { 88 | console.error("Generate questions error:", error); 89 | 90 | if (error.status === 401) { 91 | return NextResponse.json({ error: "Unauthorized: Invalid API key" }, { status: 401 }); 92 | } 93 | 94 | return NextResponse.json( 95 | { error: "Failed to generate questions", details: String(error.message) }, 96 | { status: 500 } 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /components/ToastProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | createContext, 5 | useCallback, 6 | useContext, 7 | useMemo, 8 | useRef, 9 | useState, 10 | type PropsWithChildren, 11 | } from "react"; 12 | 13 | interface ToastOptions { 14 | message: string; 15 | type?: "success" | "error" | "info"; 16 | duration?: number; 17 | } 18 | 19 | interface Toast extends Required> { 20 | id: number; 21 | } 22 | 23 | interface ToastContextValue { 24 | addToast: (options: ToastOptions) => void; 25 | } 26 | 27 | const ToastContext = createContext(undefined); 28 | 29 | export function ToastProvider({ children }: PropsWithChildren) { 30 | const [toasts, setToasts] = useState([]); 31 | const timeoutRefs = useRef>>(new Map()); 32 | 33 | const removeToast = useCallback((id: number) => { 34 | const timeoutId = timeoutRefs.current.get(id); 35 | if (timeoutId) { 36 | clearTimeout(timeoutId); 37 | timeoutRefs.current.delete(id); 38 | } 39 | setToasts((prev) => prev.filter((toast) => toast.id !== id)); 40 | }, []); 41 | 42 | const addToast = useCallback( 43 | ({ message, type = "info", duration = 4000 }: ToastOptions) => { 44 | if (!message) { 45 | return; 46 | } 47 | 48 | const id = Date.now() + Math.random(); 49 | setToasts((prev) => [...prev, { id, message, type }]); 50 | 51 | const timeoutId = setTimeout(() => { 52 | removeToast(id); 53 | }, duration); 54 | 55 | timeoutRefs.current.set(id, timeoutId); 56 | }, 57 | [removeToast] 58 | ); 59 | 60 | const contextValue = useMemo(() => ({ addToast }), [addToast]); 61 | 62 | const variantStyles = useMemo( 63 | () => ({ 64 | success: 65 | "border-green-500/80 bg-white/95 dark:bg-slate-800/95 text-slate-900 dark:text-white", 66 | error: "border-red-500/80 bg-white/95 dark:bg-slate-800/95 text-slate-900 dark:text-white", 67 | info: "border-blue-500/80 bg-white/95 dark:bg-slate-800/95 text-slate-900 dark:text-white", 68 | }), 69 | [] 70 | ); 71 | 72 | const iconClasses: Record = { 73 | success: "fas fa-check-circle text-green-500", 74 | error: "fas fa-circle-xmark text-red-500", 75 | info: "fas fa-circle-info text-blue-500", 76 | }; 77 | 78 | return ( 79 | 80 | {children} 81 |
86 | {toasts.map((toast) => ( 87 |
93 | 94 |

{toast.message}

95 | 103 |
104 | ))} 105 |
106 |
107 | ); 108 | } 109 | 110 | export function useToast(): ToastContextValue { 111 | const context = useContext(ToastContext); 112 | 113 | if (!context) { 114 | throw new Error("useToast must be used within a ToastProvider"); 115 | } 116 | 117 | return context; 118 | } 119 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect, type ReactNode } from "react"; 4 | import Link from "next/link"; 5 | import { ToastProvider } from "@/components/ToastProvider"; 6 | import { ConfirmDialogProvider } from "@/components/ConfirmDialogProvider"; 7 | import { SettingsProvider } from "@/components/SettingsProvider"; 8 | import "./globals.css"; 9 | 10 | export default function RootLayout({ children }: { children: ReactNode }) { 11 | const [isDark, setIsDark] = useState(false); 12 | const [mounted, setMounted] = useState(false); 13 | 14 | useEffect(() => { 15 | setMounted(true); 16 | const savedTheme = localStorage.getItem("theme"); 17 | const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; 18 | const shouldBeDark = savedTheme ? savedTheme === "dark" : prefersDark; 19 | 20 | setIsDark(shouldBeDark); 21 | if (shouldBeDark) { 22 | document.documentElement.classList.add("dark"); 23 | } 24 | }, []); 25 | 26 | const toggleDarkMode = () => { 27 | const newIsDark = !isDark; 28 | setIsDark(newIsDark); 29 | localStorage.setItem("theme", newIsDark ? "dark" : "light"); 30 | 31 | if (newIsDark) { 32 | document.documentElement.classList.add("dark"); 33 | } else { 34 | document.documentElement.classList.remove("dark"); 35 | } 36 | }; 37 | 38 | if (!mounted) { 39 | return ( 40 | 41 | 42 | Test Case Generator 43 | 44 | 45 | 49 | 50 | 51 |
52 |
Loading...
53 |
54 | 55 | 56 | ); 57 | } 58 | 59 | return ( 60 | 61 | 62 | Test Case Generator 63 | 64 | 65 | 69 | 70 | 71 | 72 | 73 | 74 |
75 | 80 | 81 | 82 | 94 |
95 | 96 | {children} 97 |
98 |
99 |
100 | 101 | 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /components/ConfirmDialogProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | createContext, 5 | useCallback, 6 | useContext, 7 | useMemo, 8 | useRef, 9 | useState, 10 | type PropsWithChildren, 11 | } from "react"; 12 | 13 | interface ConfirmDialogOptions { 14 | title: string; 15 | description?: string; 16 | confirmLabel?: string; 17 | cancelLabel?: string; 18 | variant?: "default" | "danger"; 19 | } 20 | 21 | interface ConfirmDialogContextValue { 22 | confirm: (options: ConfirmDialogOptions) => Promise; 23 | } 24 | 25 | interface DialogState extends ConfirmDialogOptions { 26 | open: boolean; 27 | } 28 | 29 | const defaultLabels = { 30 | confirmLabel: "Confirm", 31 | cancelLabel: "Cancel", 32 | }; 33 | 34 | const ConfirmDialogContext = createContext(undefined); 35 | 36 | export function ConfirmDialogProvider({ children }: PropsWithChildren) { 37 | const [dialogState, setDialogState] = useState(null); 38 | const resolverRef = useRef<(confirmed: boolean) => void>(); 39 | 40 | const closeDialog = useCallback((confirmed: boolean) => { 41 | setDialogState(null); 42 | resolverRef.current?.(confirmed); 43 | resolverRef.current = undefined; 44 | }, []); 45 | 46 | const confirm = useCallback((options: ConfirmDialogOptions) => { 47 | return new Promise((resolve) => { 48 | resolverRef.current = resolve; 49 | setDialogState({ 50 | open: true, 51 | title: options.title, 52 | description: options.description, 53 | variant: options.variant || "default", 54 | confirmLabel: options.confirmLabel || defaultLabels.confirmLabel, 55 | cancelLabel: options.cancelLabel || defaultLabels.cancelLabel, 56 | }); 57 | }); 58 | }, []); 59 | 60 | const contextValue = useMemo(() => ({ confirm }), [confirm]); 61 | 62 | const variantStyles = useMemo( 63 | () => ({ 64 | default: { 65 | confirm: "bg-blue-600 hover:bg-blue-700 text-white", 66 | icon: "text-blue-600", 67 | }, 68 | danger: { 69 | confirm: "bg-red-600 hover:bg-red-700 text-white", 70 | icon: "text-red-600", 71 | }, 72 | }), 73 | [] 74 | ); 75 | 76 | const activeVariant = dialogState?.variant || "default"; 77 | 78 | return ( 79 | 80 | {children} 81 | {dialogState?.open ? ( 82 |
87 |
88 |
89 | 90 | 91 | 92 |
93 |

94 | {dialogState.title} 95 |

96 | {dialogState.description && ( 97 |

98 | {dialogState.description} 99 |

100 | )} 101 |
102 |
103 |
104 | 111 | 118 |
119 |
120 |
121 | ) : null} 122 |
123 | ); 124 | } 125 | 126 | export function useConfirmDialog(): ConfirmDialogContextValue { 127 | const context = useContext(ConfirmDialogContext); 128 | 129 | if (!context) { 130 | throw new Error("useConfirmDialog must be used within a ConfirmDialogProvider"); 131 | } 132 | 133 | return context; 134 | } 135 | -------------------------------------------------------------------------------- /lib/autoContext.ts: -------------------------------------------------------------------------------- 1 | import { categoryDescriptions } from "./questions"; 2 | 3 | const categoryHints: Record = { 4 | "backend-api": [ 5 | "Validate all HTTP methods, payload schemas, and authentication flows including success, validation, and authorization failures.", 6 | "Cover rate limiting, concurrency, idempotency of repeated requests, and graceful handling of upstream/downstream outages.", 7 | "Ensure error responses follow the contract (status codes + structured error bodies).", 8 | "Verify integration with the data layer including transaction rollback, pagination, and filtering logic.", 9 | ], 10 | "frontend-component": [ 11 | "Exercise UI states: initial, loading, success, empty, and error across desktop and mobile breakpoints.", 12 | "Validate accessibility (ARIA labels, keyboard navigation, focus rings) and visual contrast in light/dark themes.", 13 | "Ensure form validation, inline feedback, and disabling/enabling of controls behaves correctly.", 14 | "Confirm external API interactions are debounced, retried, and surfaced to the user appropriately.", 15 | ], 16 | database: [ 17 | "Test CRUD operations with transactional integrity, constraints, and cascading behavior.", 18 | "Validate indexing, query performance, and plan for long-running analytical workloads.", 19 | "Exercise migration/rollback scripts and ensure backward compatibility for existing data.", 20 | "Check backup/restore procedures, failover replicas, and data retention policies.", 21 | ], 22 | "library-function": [ 23 | "Cover deterministic outputs, rounding/precision rules, and locale/timezone awareness where applicable.", 24 | "Test invalid inputs, optional arguments, and default parameter handling.", 25 | "Verify side effects, asynchronous flows, and integration with caller-supplied callbacks/promises.", 26 | "Evaluate performance for large payloads and repeated invocations to catch regressions.", 27 | ], 28 | integration: [ 29 | "Exercise full end-to-end workflows spanning multiple services or queues.", 30 | "Simulate upstream/downstream failures, retries, timeouts, and circuit breaker scenarios.", 31 | "Validate data contracts, serialization formats, and schema evolution between systems.", 32 | "Confirm monitoring/alerting signals fire when integrations degrade or drift.", 33 | ], 34 | "data-pipeline": [ 35 | "Cover ingestion of both happy-path and malformed records, ensuring data quality checks block bad data.", 36 | "Test scheduling, batching vs. streaming paths, and idempotent reprocessing of historical runs.", 37 | "Validate transformations, aggregations, and schema evolution as data moves through stages.", 38 | "Ensure failure recovery, checkpointing, and alerting operate without data loss.", 39 | ], 40 | other: [ 41 | "Exercise core business flows plus negative, edge, and boundary cases.", 42 | "Validate observability: structured logging, metrics, and tracing for key milestones.", 43 | "Ensure resiliency to dependency outages, slow responses, and retry storms.", 44 | "Cover cross-cutting concerns like security, performance, and accessibility relevant to the stack.", 45 | ], 46 | }; 47 | 48 | export function buildAutoContext( 49 | projectDescription: string, 50 | category: string, 51 | requirements?: string, 52 | defaultContext?: string 53 | ): string[] { 54 | const cleanDescription = projectDescription.trim() || "No additional description provided."; 55 | const requirementContext = requirements?.trim() 56 | ? `Key requirements: ${requirements.trim()}` 57 | : null; 58 | const globalContext = defaultContext?.trim() 59 | ? `Global considerations: ${defaultContext.trim()}` 60 | : null; 61 | const readableCategory = categoryDescriptions[category] || categoryDescriptions.other; 62 | 63 | const normalizedCategory = categoryHints[category] ? category : "other"; 64 | 65 | const baseContext = [ 66 | `Project summary: ${cleanDescription}`, 67 | `System type: ${readableCategory}.`, 68 | "Target outcome: deliver reliable, user-friendly functionality aligned with the described goals.", 69 | "Assume modern authentication/authorization, data validation, and logging best practices are required.", 70 | "Consider performance budgets, scalability, and graceful degradation under load spikes.", 71 | "Plan for operational readiness: monitoring, alerting, and fallback behavior for partial outages.", 72 | ]; 73 | 74 | const insertIndex = 1; 75 | const contextEntries = [requirementContext, globalContext].filter(Boolean) as string[]; 76 | if (contextEntries.length) { 77 | baseContext.splice(insertIndex, 0, ...contextEntries); 78 | } 79 | 80 | const combined = [...baseContext, ...categoryHints[normalizedCategory]]; 81 | 82 | // Ensure we always return a consistent number of context items for the generator 83 | return combined.slice(0, 10); 84 | } 85 | -------------------------------------------------------------------------------- /components/SavedTestsList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { useRouter } from "next/navigation"; 5 | import { useToast } from "@/components/ToastProvider"; 6 | import { useConfirmDialog } from "@/components/ConfirmDialogProvider"; 7 | 8 | interface SavedTest { 9 | id: string; 10 | projectDescription: string; 11 | category: string; 12 | timestamp: string; 13 | testCaseCount: number; 14 | } 15 | 16 | interface SavedTestsListProps { 17 | onLoadTest: (testId: string) => void; 18 | } 19 | 20 | export default function SavedTestsList({ onLoadTest }: SavedTestsListProps) { 21 | const [savedTests, setSavedTests] = useState([]); 22 | const [showList, setShowList] = useState(false); 23 | const router = useRouter(); 24 | const { addToast } = useToast(); 25 | const { confirm } = useConfirmDialog(); 26 | 27 | useEffect(() => { 28 | loadSavedTests(); 29 | }, []); 30 | 31 | const loadSavedTests = () => { 32 | try { 33 | const saved = localStorage.getItem("savedTests"); 34 | if (saved) { 35 | const tests = JSON.parse(saved); 36 | setSavedTests( 37 | tests.sort( 38 | (a: SavedTest, b: SavedTest) => 39 | new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() 40 | ) 41 | ); 42 | } 43 | } catch (error) { 44 | console.error("Error loading saved tests:", error); 45 | } 46 | }; 47 | 48 | const handleLoadTest = (testId: string) => { 49 | try { 50 | const saved = localStorage.getItem(testId); 51 | if (saved) { 52 | const testData = JSON.parse(saved); 53 | // Restore the session storage with the saved data 54 | sessionStorage.setItem("projectDescription", testData.projectDescription); 55 | sessionStorage.setItem("category", testData.category); 56 | sessionStorage.setItem("testCases", JSON.stringify(testData.testCases)); 57 | sessionStorage.setItem("testingStrategy", testData.testingStrategy); 58 | sessionStorage.setItem("riskAreas", JSON.stringify(testData.riskAreas)); 59 | sessionStorage.setItem("currentTestId", testId); 60 | onLoadTest(testId); 61 | router.push("/results"); 62 | } 63 | } catch (error) { 64 | console.error("Error loading test:", error); 65 | addToast({ message: "Failed to load test.", type: "error" }); 66 | } 67 | }; 68 | 69 | const handleDeleteTest = async (testId: string) => { 70 | const confirmed = await confirm({ 71 | title: "Delete saved test?", 72 | description: 73 | "This test will be permanently removed from your saved list. This action cannot be undone.", 74 | confirmLabel: "Delete test", 75 | variant: "danger", 76 | }); 77 | 78 | if (!confirmed) { 79 | return; 80 | } 81 | 82 | try { 83 | localStorage.removeItem(testId); 84 | const updated = savedTests.filter((test) => test.id !== testId); 85 | setSavedTests(updated); 86 | localStorage.setItem("savedTests", JSON.stringify(updated)); 87 | addToast({ message: "Saved test deleted.", type: "success" }); 88 | } catch (error) { 89 | console.error("Error deleting test:", error); 90 | addToast({ message: "Failed to delete test.", type: "error" }); 91 | } 92 | }; 93 | 94 | if (savedTests.length === 0) { 95 | return null; 96 | } 97 | 98 | return ( 99 |
100 | 108 | 109 | {showList && ( 110 |
111 | {savedTests.map((test) => ( 112 |
116 |
117 |
118 |

122 | {test.projectDescription} 123 |

124 |

125 | Category: {test.category} 126 |

127 |

128 | {test.testCaseCount} test cases •{" "} 129 | {new Date(test.timestamp).toLocaleDateString()} at{" "} 130 | {new Date(test.timestamp).toLocaleTimeString([], { 131 | hour: "2-digit", 132 | minute: "2-digit", 133 | })} 134 |

135 |
136 |
137 | 145 | 153 |
154 |
155 |
156 | ))} 157 |
158 | )} 159 |
160 | ); 161 | } 162 | -------------------------------------------------------------------------------- /app/settings/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { useRouter } from "next/navigation"; 5 | import { useSettings } from "@/components/SettingsProvider"; 6 | import { useToast } from "@/components/ToastProvider"; 7 | import { boilerplateOptions, defaultSettings, testTypeOptions } from "@/lib/settings"; 8 | 9 | export default function SettingsPage() { 10 | const router = useRouter(); 11 | const { settings, updateSettings } = useSettings(); 12 | const { addToast } = useToast(); 13 | const [formState, setFormState] = useState(settings); 14 | 15 | useEffect(() => { 16 | setFormState(settings); 17 | }, [settings]); 18 | 19 | const handleSave = () => { 20 | updateSettings(formState); 21 | addToast({ message: "Settings saved successfully.", type: "success" }); 22 | }; 23 | 24 | const handleReset = () => { 25 | updateSettings(defaultSettings); 26 | addToast({ message: "Settings reset to defaults.", type: "info" }); 27 | }; 28 | 29 | return ( 30 |
31 |
32 | 39 | 40 |
41 |
42 |

Settings

43 |

44 | Personalize how we generate context, test cases, and boilerplate. 45 |

46 |
47 | 48 |
49 |
50 |

51 | Default context 52 |

53 |

54 | This text is automatically appended to every request to emphasize permanent testing 55 | requirements (e.g. compliance rules, performance SLAs). 56 |

57 |