diff --git a/src/components/UnitTestDesigner.tsx b/src/components/UnitTestDesigner.tsx index 6857755..a121c5f 100644 --- a/src/components/UnitTestDesigner.tsx +++ b/src/components/UnitTestDesigner.tsx @@ -2,16 +2,13 @@ import { useState } from 'react' import { UnitTest, TestCase } from '@/types/project' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { ScrollArea } from '@/components/ui/scroll-area' -import { Textarea } from '@/components/ui/textarea' -import { Plus, Trash, Flask, Sparkle } from '@phosphor-icons/react' +import { Plus, Flask } from '@phosphor-icons/react' import { toast } from 'sonner' -import { Badge } from '@/components/ui/badge' +import unitTestDesignerCopy from '@/data/unit-test-designer.json' +import { TestSuiteList } from '@/components/unit-test-designer/TestSuiteList' +import { TestSuiteEditor } from '@/components/unit-test-designer/TestSuiteEditor' +import { TestCasesPanel } from '@/components/unit-test-designer/TestCasesPanel' interface UnitTestDesignerProps { tests: UnitTest[] @@ -25,7 +22,7 @@ export function UnitTestDesigner({ tests, onTestsChange }: UnitTestDesignerProps const handleAddTest = () => { const newTest: UnitTest = { id: `unit-test-${Date.now()}`, - name: 'New Test Suite', + name: unitTestDesignerCopy.defaults.testSuiteName, description: '', testType: 'component', targetFile: '', @@ -53,8 +50,8 @@ export function UnitTestDesigner({ tests, onTestsChange }: UnitTestDesignerProps if (!selectedTest) return const newCase: TestCase = { id: `case-${Date.now()}`, - description: 'should work correctly', - assertions: ['expect(...).toBe(...)'], + description: unitTestDesignerCopy.defaults.testCaseDescription, + assertions: [unitTestDesignerCopy.defaults.assertion], setup: '', teardown: '' } @@ -82,7 +79,7 @@ export function UnitTestDesigner({ tests, onTestsChange }: UnitTestDesignerProps const testCase = selectedTest.testCases.find(c => c.id === caseId) if (!testCase) return handleUpdateTestCase(caseId, { - assertions: [...testCase.assertions, 'expect(...).toBe(...)'] + assertions: [...testCase.assertions, unitTestDesignerCopy.defaults.assertion] }) } @@ -105,289 +102,64 @@ export function UnitTestDesigner({ tests, onTestsChange }: UnitTestDesignerProps } const handleGenerateWithAI = async () => { - const description = prompt('Describe the component/function you want to test:') + const description = prompt(unitTestDesignerCopy.prompts.generateDescription) if (!description) return try { - toast.info('Generating test with AI...') - const promptText = `You are a unit test generator. Create tests based on: "${description}" - -Return a valid JSON object with a single property "test": -{ - "test": { - "id": "unique-id", - "name": "ComponentName/FunctionName Tests", - "description": "Test suite description", - "testType": "component" | "function" | "hook" | "integration", - "targetFile": "/path/to/file.tsx", - "testCases": [ - { - "id": "case-id", - "description": "should render correctly", - "assertions": [ - "expect(screen.getByText('Hello')).toBeInTheDocument()", - "expect(result).toBe(true)" - ], - "setup": "const { getByText } = render()", - "teardown": "cleanup()" - } - ] - } -} - -Create comprehensive test cases with appropriate assertions for React Testing Library or Vitest.` - + toast.info(unitTestDesignerCopy.toasts.generating) + const promptText = unitTestDesignerCopy.prompts.generatePromptTemplate.replace( + '{{description}}', + description + ) const response = await window.spark.llm(promptText, 'gpt-4o', true) const parsed = JSON.parse(response) onTestsChange([...tests, parsed.test]) setSelectedTestId(parsed.test.id) - toast.success('Test suite generated successfully!') + toast.success(unitTestDesignerCopy.toasts.generateSuccess) } catch (error) { console.error(error) - toast.error('Failed to generate test') + toast.error(unitTestDesignerCopy.toasts.generateError) } } - const getTestTypeColor = (type: string) => { - const colors: Record = { - component: 'bg-blue-500', - function: 'bg-green-500', - hook: 'bg-purple-500', - integration: 'bg-orange-500' - } - return colors[type] || 'bg-gray-500' - } - return ( - - - Test Suites - - - - - - - - - - - - {tests.map(test => ( - setSelectedTestId(test.id)} - > - - - - {test.name} - - {test.targetFile || 'No file'} - {test.testCases.length} cases - - { - e.stopPropagation() - handleDeleteTest(test.id) - }} - > - - - - ))} - {tests.length === 0 && ( - - No test suites yet. Click + to create one. - - )} - - - + {selectedTest ? ( - - Test Suite Configuration - - - Run Tests - - - - - - Test Suite Details - - - - Test Suite Name - handleUpdateTest(selectedTest.id, { name: e.target.value })} - /> - - - Description - handleUpdateTest(selectedTest.id, { description: e.target.value })} - placeholder="What does this test suite cover?" - /> - - - - Test Type - handleUpdateTest(selectedTest.id, { testType: value })} - > - - - - - Component - Function - Hook - Integration - - - - - Target File - handleUpdateTest(selectedTest.id, { targetFile: e.target.value })} - placeholder="/src/components/Button.tsx" - /> - - - - - - - - - - Test Cases - Define individual test cases - - - - Add Test Case - - - - - - - {selectedTest.testCases.map((testCase, index) => ( - - - - Case {index + 1} - handleDeleteTestCase(testCase.id)} - > - - - - - Description (it...) - handleUpdateTestCase(testCase.id, { description: e.target.value })} - placeholder="should render correctly" - /> - - - Setup Code (optional) - handleUpdateTestCase(testCase.id, { setup: e.target.value })} - placeholder="const { getByText } = render()" - className="font-mono text-xs" - rows={2} - /> - - - - Assertions - handleAddAssertion(testCase.id)} - > - - - - - {testCase.assertions.map((assertion, assertionIndex) => ( - - handleUpdateAssertion(testCase.id, assertionIndex, e.target.value)} - placeholder="expect(...).toBe(...)" - className="font-mono text-xs" - /> - handleDeleteAssertion(testCase.id, assertionIndex)} - > - - - - ))} - - - - Teardown Code (optional) - handleUpdateTestCase(testCase.id, { teardown: e.target.value })} - placeholder="cleanup()" - className="font-mono text-xs" - rows={2} - /> - - - - ))} - {selectedTest.testCases.length === 0 && ( - - No test cases yet. Click "Add Test Case" to create one. - - )} - - - - + + ) : ( - No test suite selected + + {unitTestDesignerCopy.labels.noTestSuiteSelected} + - Create or select a test suite to configure + {unitTestDesignerCopy.labels.noTestSuiteSelectedBody} - Create Test Suite + {unitTestDesignerCopy.labels.createTestSuite} diff --git a/src/components/unit-test-designer/TestCasesPanel.tsx b/src/components/unit-test-designer/TestCasesPanel.tsx new file mode 100644 index 0000000..ea37bef --- /dev/null +++ b/src/components/unit-test-designer/TestCasesPanel.tsx @@ -0,0 +1,133 @@ +import { TestCase } from '@/types/project' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Textarea } from '@/components/ui/textarea' +import { Plus, Trash } from '@phosphor-icons/react' +import unitTestDesignerCopy from '@/data/unit-test-designer.json' + +interface TestCasesPanelProps { + testCases: TestCase[] + onAddTestCase: () => void + onDeleteTestCase: (caseId: string) => void + onUpdateTestCase: (caseId: string, updates: Partial) => void + onAddAssertion: (caseId: string) => void + onUpdateAssertion: (caseId: string, index: number, value: string) => void + onDeleteAssertion: (caseId: string, index: number) => void +} + +export function TestCasesPanel({ + testCases, + onAddTestCase, + onDeleteTestCase, + onUpdateTestCase, + onAddAssertion, + onUpdateAssertion, + onDeleteAssertion +}: TestCasesPanelProps) { + return ( + + + + + {unitTestDesignerCopy.labels.testCases} + {unitTestDesignerCopy.labels.testCasesDescription} + + + + {unitTestDesignerCopy.labels.addTestCase} + + + + + + + {testCases.map((testCase, index) => ( + + + + + {unitTestDesignerCopy.labels.case} {index + 1} + + onDeleteTestCase(testCase.id)} + > + + + + + {unitTestDesignerCopy.labels.caseDescription} + onUpdateTestCase(testCase.id, { description: event.target.value })} + placeholder={unitTestDesignerCopy.placeholders.caseDescription} + /> + + + {unitTestDesignerCopy.labels.setupCode} + onUpdateTestCase(testCase.id, { setup: event.target.value })} + placeholder={unitTestDesignerCopy.placeholders.setupCode} + className="font-mono text-xs" + rows={2} + /> + + + + {unitTestDesignerCopy.labels.assertions} + onAddAssertion(testCase.id)}> + + + + + {testCase.assertions.map((assertion, assertionIndex) => ( + + + onUpdateAssertion(testCase.id, assertionIndex, event.target.value) + } + placeholder={unitTestDesignerCopy.placeholders.assertion} + className="font-mono text-xs" + /> + onDeleteAssertion(testCase.id, assertionIndex)} + > + + + + ))} + + + + {unitTestDesignerCopy.labels.teardownCode} + onUpdateTestCase(testCase.id, { teardown: event.target.value })} + placeholder={unitTestDesignerCopy.placeholders.teardownCode} + className="font-mono text-xs" + rows={2} + /> + + + + ))} + {testCases.length === 0 && ( + + {unitTestDesignerCopy.labels.noTestCasesYet} + + )} + + + + + ) +} diff --git a/src/components/unit-test-designer/TestSuiteEditor.tsx b/src/components/unit-test-designer/TestSuiteEditor.tsx new file mode 100644 index 0000000..f543ea0 --- /dev/null +++ b/src/components/unit-test-designer/TestSuiteEditor.tsx @@ -0,0 +1,81 @@ +import { UnitTest } from '@/types/project' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Textarea } from '@/components/ui/textarea' +import { Flask } from '@phosphor-icons/react' +import unitTestDesignerCopy from '@/data/unit-test-designer.json' + +interface TestSuiteEditorProps { + test: UnitTest + onUpdateTest: (testId: string, updates: Partial) => void +} + +export function TestSuiteEditor({ test, onUpdateTest }: TestSuiteEditorProps) { + return ( + + + {unitTestDesignerCopy.labels.testSuiteConfiguration} + + + {unitTestDesignerCopy.labels.runTests} + + + + + + {unitTestDesignerCopy.labels.testSuiteDetails} + + + + {unitTestDesignerCopy.labels.testSuiteName} + onUpdateTest(test.id, { name: event.target.value })} + /> + + + {unitTestDesignerCopy.labels.description} + onUpdateTest(test.id, { description: event.target.value })} + placeholder={unitTestDesignerCopy.placeholders.testSuiteDescription} + /> + + + + {unitTestDesignerCopy.labels.testType} + onUpdateTest(test.id, { testType: value })} + > + + + + + {unitTestDesignerCopy.testTypes.component} + {unitTestDesignerCopy.testTypes.function} + {unitTestDesignerCopy.testTypes.hook} + {unitTestDesignerCopy.testTypes.integration} + + + + + {unitTestDesignerCopy.labels.targetFile} + onUpdateTest(test.id, { targetFile: event.target.value })} + placeholder={unitTestDesignerCopy.placeholders.targetFile} + /> + + + + + + ) +} diff --git a/src/components/unit-test-designer/TestSuiteList.tsx b/src/components/unit-test-designer/TestSuiteList.tsx new file mode 100644 index 0000000..1839900 --- /dev/null +++ b/src/components/unit-test-designer/TestSuiteList.tsx @@ -0,0 +1,91 @@ +import { UnitTest } from '@/types/project' +import { Button } from '@/components/ui/button' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Plus, Sparkle, Trash } from '@phosphor-icons/react' +import unitTestDesignerCopy from '@/data/unit-test-designer.json' + +interface TestSuiteListProps { + tests: UnitTest[] + selectedTestId: string | null + onSelectTest: (testId: string) => void + onAddTest: () => void + onDeleteTest: (testId: string) => void + onGenerateWithAI: () => void +} + +const getTestTypeColor = (type: string) => { + const colors: Record = { + component: 'bg-blue-500', + function: 'bg-green-500', + hook: 'bg-purple-500', + integration: 'bg-orange-500' + } + return colors[type] || 'bg-gray-500' +} + +export function TestSuiteList({ + tests, + selectedTestId, + onSelectTest, + onAddTest, + onDeleteTest, + onGenerateWithAI +}: TestSuiteListProps) { + return ( + + + {unitTestDesignerCopy.labels.testSuites} + + + + + + + + + + + + {tests.map(test => ( + onSelectTest(test.id)} + > + + + + {test.name} + + + {test.targetFile || unitTestDesignerCopy.labels.noFile} + + + {test.testCases.length} {unitTestDesignerCopy.labels.casesSuffix} + + + { + event.stopPropagation() + onDeleteTest(test.id) + }} + > + + + + ))} + {tests.length === 0 && ( + + {unitTestDesignerCopy.labels.noTestSuitesYet} + + )} + + + + ) +} diff --git a/src/data/unit-test-designer.json b/src/data/unit-test-designer.json new file mode 100644 index 0000000..7c00c7e --- /dev/null +++ b/src/data/unit-test-designer.json @@ -0,0 +1,55 @@ +{ + "labels": { + "testSuites": "Test Suites", + "testSuiteConfiguration": "Test Suite Configuration", + "testSuiteDetails": "Test Suite Details", + "testSuiteName": "Test Suite Name", + "description": "Description", + "testType": "Test Type", + "targetFile": "Target File", + "testCases": "Test Cases", + "testCasesDescription": "Define individual test cases", + "case": "Case", + "caseDescription": "Description (it...)", + "setupCode": "Setup Code (optional)", + "assertions": "Assertions", + "teardownCode": "Teardown Code (optional)", + "runTests": "Run Tests", + "addTestCase": "Add Test Case", + "createTestSuite": "Create Test Suite", + "noTestSuiteSelected": "No test suite selected", + "noTestSuiteSelectedBody": "Create or select a test suite to configure", + "noTestSuitesYet": "No test suites yet. Click + to create one.", + "noTestCasesYet": "No test cases yet. Click \"Add Test Case\" to create one.", + "noFile": "No file", + "casesSuffix": "cases" + }, + "placeholders": { + "testSuiteDescription": "What does this test suite cover?", + "targetFile": "/src/components/Button.tsx", + "caseDescription": "should render correctly", + "setupCode": "const { getByText } = render()", + "assertion": "expect(...).toBe(...)", + "teardownCode": "cleanup()" + }, + "defaults": { + "testSuiteName": "New Test Suite", + "testCaseDescription": "should work correctly", + "assertion": "expect(...).toBe(...)" + }, + "prompts": { + "generateDescription": "Describe the component/function you want to test:", + "generatePromptTemplate": "You are a unit test generator. Create tests based on: \"{{description}}\"\n\nReturn a valid JSON object with a single property \"test\":\n{\n \"test\": {\n \"id\": \"unique-id\",\n \"name\": \"ComponentName/FunctionName Tests\",\n \"description\": \"Test suite description\",\n \"testType\": \"component\" | \"function\" | \"hook\" | \"integration\",\n \"targetFile\": \"/path/to/file.tsx\",\n \"testCases\": [\n {\n \"id\": \"case-id\",\n \"description\": \"should render correctly\",\n \"assertions\": [\n \"expect(screen.getByText('Hello')).toBeInTheDocument()\",\n \"expect(result).toBe(true)\"\n ],\n \"setup\": \"const { getByText } = render()\",\n \"teardown\": \"cleanup()\"\n }\n ]\n }\n}\n\nCreate comprehensive test cases with appropriate assertions for React Testing Library or Vitest." + }, + "toasts": { + "generating": "Generating test with AI...", + "generateSuccess": "Test suite generated successfully!", + "generateError": "Failed to generate test" + }, + "testTypes": { + "component": "Component", + "function": "Function", + "hook": "Hook", + "integration": "Integration" + } +}
No test suite selected
+ {unitTestDesignerCopy.labels.noTestSuiteSelected} +
- Create or select a test suite to configure + {unitTestDesignerCopy.labels.noTestSuiteSelectedBody}