mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Refactor unit test designer panels
This commit is contained in:
@@ -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(<Component />)",
|
||||
"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<string, string> = {
|
||||
component: 'bg-blue-500',
|
||||
function: 'bg-green-500',
|
||||
hook: 'bg-purple-500',
|
||||
integration: 'bg-orange-500'
|
||||
}
|
||||
return colors[type] || 'bg-gray-500'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex">
|
||||
<div className="w-80 border-r border-border bg-card">
|
||||
<div className="p-4 border-b border-border flex items-center justify-between">
|
||||
<h2 className="font-semibold text-sm">Test Suites</h2>
|
||||
<div className="flex gap-1">
|
||||
<Button size="sm" variant="outline" onClick={handleGenerateWithAI}>
|
||||
<Sparkle size={14} weight="duotone" />
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleAddTest}>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="h-[calc(100vh-200px)]">
|
||||
<div className="p-2 space-y-1">
|
||||
{tests.map(test => (
|
||||
<div
|
||||
key={test.id}
|
||||
className={`p-3 rounded-md cursor-pointer flex items-start justify-between group ${
|
||||
selectedTestId === test.id ? 'bg-accent text-accent-foreground' : 'hover:bg-muted'
|
||||
}`}
|
||||
onClick={() => setSelectedTestId(test.id)}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className={`w-2 h-2 rounded-full ${getTestTypeColor(test.testType)}`} />
|
||||
<div className="font-medium text-sm truncate">{test.name}</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground truncate">{test.targetFile || 'No file'}</div>
|
||||
<div className="text-xs text-muted-foreground">{test.testCases.length} cases</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteTest(test.id)
|
||||
}}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{tests.length === 0 && (
|
||||
<div className="p-8 text-center text-sm text-muted-foreground">
|
||||
No test suites yet. Click + to create one.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
<TestSuiteList
|
||||
tests={tests}
|
||||
selectedTestId={selectedTestId}
|
||||
onSelectTest={setSelectedTestId}
|
||||
onAddTest={handleAddTest}
|
||||
onDeleteTest={handleDeleteTest}
|
||||
onGenerateWithAI={handleGenerateWithAI}
|
||||
/>
|
||||
|
||||
<div className="flex-1 p-6">
|
||||
{selectedTest ? (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">Test Suite Configuration</h2>
|
||||
<Button variant="outline">
|
||||
<Flask size={16} className="mr-2" weight="fill" />
|
||||
Run Tests
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Test Suite Details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="test-name">Test Suite Name</Label>
|
||||
<Input
|
||||
id="test-name"
|
||||
value={selectedTest.name}
|
||||
onChange={e => handleUpdateTest(selectedTest.id, { name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="test-description">Description</Label>
|
||||
<Textarea
|
||||
id="test-description"
|
||||
value={selectedTest.description}
|
||||
onChange={e => handleUpdateTest(selectedTest.id, { description: e.target.value })}
|
||||
placeholder="What does this test suite cover?"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="test-type">Test Type</Label>
|
||||
<Select
|
||||
value={selectedTest.testType}
|
||||
onValueChange={(value: any) => handleUpdateTest(selectedTest.id, { testType: value })}
|
||||
>
|
||||
<SelectTrigger id="test-type">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="component">Component</SelectItem>
|
||||
<SelectItem value="function">Function</SelectItem>
|
||||
<SelectItem value="hook">Hook</SelectItem>
|
||||
<SelectItem value="integration">Integration</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="target-file">Target File</Label>
|
||||
<Input
|
||||
id="target-file"
|
||||
value={selectedTest.targetFile}
|
||||
onChange={e => handleUpdateTest(selectedTest.id, { targetFile: e.target.value })}
|
||||
placeholder="/src/components/Button.tsx"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Test Cases</CardTitle>
|
||||
<CardDescription>Define individual test cases</CardDescription>
|
||||
</div>
|
||||
<Button size="sm" onClick={handleAddTestCase}>
|
||||
<Plus size={14} className="mr-1" />
|
||||
Add Test Case
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[450px]">
|
||||
<div className="space-y-4">
|
||||
{selectedTest.testCases.map((testCase, index) => (
|
||||
<Card key={testCase.id}>
|
||||
<CardContent className="pt-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="outline">Case {index + 1}</Badge>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleDeleteTestCase(testCase.id)}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Description (it...)</Label>
|
||||
<Input
|
||||
value={testCase.description}
|
||||
onChange={e => handleUpdateTestCase(testCase.id, { description: e.target.value })}
|
||||
placeholder="should render correctly"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Setup Code (optional)</Label>
|
||||
<Textarea
|
||||
value={testCase.setup || ''}
|
||||
onChange={e => handleUpdateTestCase(testCase.id, { setup: e.target.value })}
|
||||
placeholder="const { getByText } = render(<Component />)"
|
||||
className="font-mono text-xs"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Assertions</Label>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleAddAssertion(testCase.id)}
|
||||
>
|
||||
<Plus size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{testCase.assertions.map((assertion, assertionIndex) => (
|
||||
<div key={assertionIndex} className="flex gap-2">
|
||||
<Input
|
||||
value={assertion}
|
||||
onChange={e => handleUpdateAssertion(testCase.id, assertionIndex, e.target.value)}
|
||||
placeholder="expect(...).toBe(...)"
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleDeleteAssertion(testCase.id, assertionIndex)}
|
||||
>
|
||||
<Trash size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Teardown Code (optional)</Label>
|
||||
<Textarea
|
||||
value={testCase.teardown || ''}
|
||||
onChange={e => handleUpdateTestCase(testCase.id, { teardown: e.target.value })}
|
||||
placeholder="cleanup()"
|
||||
className="font-mono text-xs"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
{selectedTest.testCases.length === 0 && (
|
||||
<div className="py-12 text-center text-sm text-muted-foreground">
|
||||
No test cases yet. Click "Add Test Case" to create one.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TestSuiteEditor test={selectedTest} onUpdateTest={handleUpdateTest} />
|
||||
<TestCasesPanel
|
||||
testCases={selectedTest.testCases}
|
||||
onAddTestCase={handleAddTestCase}
|
||||
onDeleteTestCase={handleDeleteTestCase}
|
||||
onUpdateTestCase={handleUpdateTestCase}
|
||||
onAddAssertion={handleAddAssertion}
|
||||
onUpdateAssertion={handleUpdateAssertion}
|
||||
onDeleteAssertion={handleDeleteAssertion}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Flask size={48} className="mx-auto mb-4 text-muted-foreground" />
|
||||
<p className="text-lg font-medium mb-2">No test suite selected</p>
|
||||
<p className="text-lg font-medium mb-2">
|
||||
{unitTestDesignerCopy.labels.noTestSuiteSelected}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Create or select a test suite to configure
|
||||
{unitTestDesignerCopy.labels.noTestSuiteSelectedBody}
|
||||
</p>
|
||||
<Button onClick={handleAddTest}>
|
||||
<Plus size={16} className="mr-2" />
|
||||
Create Test Suite
|
||||
{unitTestDesignerCopy.labels.createTestSuite}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
133
src/components/unit-test-designer/TestCasesPanel.tsx
Normal file
133
src/components/unit-test-designer/TestCasesPanel.tsx
Normal file
@@ -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<TestCase>) => 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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>{unitTestDesignerCopy.labels.testCases}</CardTitle>
|
||||
<CardDescription>{unitTestDesignerCopy.labels.testCasesDescription}</CardDescription>
|
||||
</div>
|
||||
<Button size="sm" onClick={onAddTestCase}>
|
||||
<Plus size={14} className="mr-1" />
|
||||
{unitTestDesignerCopy.labels.addTestCase}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[450px]">
|
||||
<div className="space-y-4">
|
||||
{testCases.map((testCase, index) => (
|
||||
<Card key={testCase.id}>
|
||||
<CardContent className="pt-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="outline">
|
||||
{unitTestDesignerCopy.labels.case} {index + 1}
|
||||
</Badge>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onDeleteTestCase(testCase.id)}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{unitTestDesignerCopy.labels.caseDescription}</Label>
|
||||
<Input
|
||||
value={testCase.description}
|
||||
onChange={event => onUpdateTestCase(testCase.id, { description: event.target.value })}
|
||||
placeholder={unitTestDesignerCopy.placeholders.caseDescription}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{unitTestDesignerCopy.labels.setupCode}</Label>
|
||||
<Textarea
|
||||
value={testCase.setup || ''}
|
||||
onChange={event => onUpdateTestCase(testCase.id, { setup: event.target.value })}
|
||||
placeholder={unitTestDesignerCopy.placeholders.setupCode}
|
||||
className="font-mono text-xs"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>{unitTestDesignerCopy.labels.assertions}</Label>
|
||||
<Button size="sm" variant="outline" onClick={() => onAddAssertion(testCase.id)}>
|
||||
<Plus size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{testCase.assertions.map((assertion, assertionIndex) => (
|
||||
<div key={assertionIndex} className="flex gap-2">
|
||||
<Input
|
||||
value={assertion}
|
||||
onChange={event =>
|
||||
onUpdateAssertion(testCase.id, assertionIndex, event.target.value)
|
||||
}
|
||||
placeholder={unitTestDesignerCopy.placeholders.assertion}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onDeleteAssertion(testCase.id, assertionIndex)}
|
||||
>
|
||||
<Trash size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{unitTestDesignerCopy.labels.teardownCode}</Label>
|
||||
<Textarea
|
||||
value={testCase.teardown || ''}
|
||||
onChange={event => onUpdateTestCase(testCase.id, { teardown: event.target.value })}
|
||||
placeholder={unitTestDesignerCopy.placeholders.teardownCode}
|
||||
className="font-mono text-xs"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
{testCases.length === 0 && (
|
||||
<div className="py-12 text-center text-sm text-muted-foreground">
|
||||
{unitTestDesignerCopy.labels.noTestCasesYet}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
81
src/components/unit-test-designer/TestSuiteEditor.tsx
Normal file
81
src/components/unit-test-designer/TestSuiteEditor.tsx
Normal file
@@ -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<UnitTest>) => void
|
||||
}
|
||||
|
||||
export function TestSuiteEditor({ test, onUpdateTest }: TestSuiteEditorProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">{unitTestDesignerCopy.labels.testSuiteConfiguration}</h2>
|
||||
<Button variant="outline">
|
||||
<Flask size={16} className="mr-2" weight="fill" />
|
||||
{unitTestDesignerCopy.labels.runTests}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{unitTestDesignerCopy.labels.testSuiteDetails}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="test-name">{unitTestDesignerCopy.labels.testSuiteName}</Label>
|
||||
<Input
|
||||
id="test-name"
|
||||
value={test.name}
|
||||
onChange={event => onUpdateTest(test.id, { name: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="test-description">{unitTestDesignerCopy.labels.description}</Label>
|
||||
<Textarea
|
||||
id="test-description"
|
||||
value={test.description}
|
||||
onChange={event => onUpdateTest(test.id, { description: event.target.value })}
|
||||
placeholder={unitTestDesignerCopy.placeholders.testSuiteDescription}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="test-type">{unitTestDesignerCopy.labels.testType}</Label>
|
||||
<Select
|
||||
value={test.testType}
|
||||
onValueChange={(value: UnitTest['testType']) => onUpdateTest(test.id, { testType: value })}
|
||||
>
|
||||
<SelectTrigger id="test-type">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="component">{unitTestDesignerCopy.testTypes.component}</SelectItem>
|
||||
<SelectItem value="function">{unitTestDesignerCopy.testTypes.function}</SelectItem>
|
||||
<SelectItem value="hook">{unitTestDesignerCopy.testTypes.hook}</SelectItem>
|
||||
<SelectItem value="integration">{unitTestDesignerCopy.testTypes.integration}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="target-file">{unitTestDesignerCopy.labels.targetFile}</Label>
|
||||
<Input
|
||||
id="target-file"
|
||||
value={test.targetFile}
|
||||
onChange={event => onUpdateTest(test.id, { targetFile: event.target.value })}
|
||||
placeholder={unitTestDesignerCopy.placeholders.targetFile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
91
src/components/unit-test-designer/TestSuiteList.tsx
Normal file
91
src/components/unit-test-designer/TestSuiteList.tsx
Normal file
@@ -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<string, string> = {
|
||||
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 (
|
||||
<div className="w-80 border-r border-border bg-card">
|
||||
<div className="p-4 border-b border-border flex items-center justify-between">
|
||||
<h2 className="font-semibold text-sm">{unitTestDesignerCopy.labels.testSuites}</h2>
|
||||
<div className="flex gap-1">
|
||||
<Button size="sm" variant="outline" onClick={onGenerateWithAI}>
|
||||
<Sparkle size={14} weight="duotone" />
|
||||
</Button>
|
||||
<Button size="sm" onClick={onAddTest}>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="h-[calc(100vh-200px)]">
|
||||
<div className="p-2 space-y-1">
|
||||
{tests.map(test => (
|
||||
<div
|
||||
key={test.id}
|
||||
className={`p-3 rounded-md cursor-pointer flex items-start justify-between group ${
|
||||
selectedTestId === test.id ? 'bg-accent text-accent-foreground' : 'hover:bg-muted'
|
||||
}`}
|
||||
onClick={() => onSelectTest(test.id)}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className={`w-2 h-2 rounded-full ${getTestTypeColor(test.testType)}`} />
|
||||
<div className="font-medium text-sm truncate">{test.name}</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground truncate">
|
||||
{test.targetFile || unitTestDesignerCopy.labels.noFile}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{test.testCases.length} {unitTestDesignerCopy.labels.casesSuffix}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onDeleteTest(test.id)
|
||||
}}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{tests.length === 0 && (
|
||||
<div className="p-8 text-center text-sm text-muted-foreground">
|
||||
{unitTestDesignerCopy.labels.noTestSuitesYet}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
55
src/data/unit-test-designer.json
Normal file
55
src/data/unit-test-designer.json
Normal file
@@ -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(<Component />)",
|
||||
"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(<Component />)\",\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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user