pmtool / src /components /test-cases /GeneratedTestCasesPreview.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
18.5 kB
/**
* Generated Test Cases Preview Modal
*
* Shows AI-generated test cases before saving.
* User can review, edit, and selectively save test cases.
*/
import React, { useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import { Loader2, CheckCircle, XCircle, Save, Sparkles, Edit, ChevronDown, ChevronUp } from "lucide-react";
import { toast } from "@/lib/custom-toast";
import { FrontendTestCase } from "@/services/aiTestCaseApi";
import { tasksApi } from "@/services/tasksApi";
interface GeneratedTestCasesPreviewProps {
open: boolean;
onOpenChange: (open: boolean) => void;
testCases: FrontendTestCase[];
featureId: number;
onSaveComplete: () => void;
}
const GeneratedTestCasesPreview: React.FC<GeneratedTestCasesPreviewProps> = ({
open,
onOpenChange,
testCases,
featureId,
onSaveComplete,
}) => {
const [selectedCases, setSelectedCases] = useState<Set<number>>(new Set(testCases.map((_, i) => i)));
const [savingStates, setSavingStates] = useState<Record<number, 'idle' | 'saving' | 'saved' | 'error'>>({});
const [isSavingAll, setIsSavingAll] = useState(false);
const [expandedCases, setExpandedCases] = useState<Set<number>>(new Set([0])); // First case expanded by default
const [editedCases, setEditedCases] = useState<Record<number, FrontendTestCase>>({});
// Toggle selection
const toggleSelection = (index: number) => {
const newSelected = new Set(selectedCases);
if (newSelected.has(index)) {
newSelected.delete(index);
} else {
newSelected.add(index);
}
setSelectedCases(newSelected);
};
// Toggle expand/collapse
const toggleExpand = (index: number) => {
const newExpanded = new Set(expandedCases);
if (newExpanded.has(index)) {
newExpanded.delete(index);
} else {
newExpanded.add(index);
}
setExpandedCases(newExpanded);
};
// Select/deselect all
const toggleSelectAll = () => {
if (selectedCases.size === testCases.length) {
setSelectedCases(new Set());
} else {
setSelectedCases(new Set(testCases.map((_, i) => i)));
}
};
// Update edited test case
const updateTestCase = (index: number, field: keyof FrontendTestCase, value: string) => {
setEditedCases(prev => ({
...prev,
[index]: {
...(editedCases[index] || testCases[index]),
[field]: value,
},
}));
};
// Save single test case
const saveTestCase = async (index: number) => {
const testCase = editedCases[index] || testCases[index];
setSavingStates(prev => ({ ...prev, [index]: 'saving' }));
try {
const taskData = {
type: 'Test Case',
title: testCase.title,
description: testCase.description,
issuesId: featureId,
projectId: testCase.projectId,
status: testCase.status || 'Draft',
priority: testCase.priority || 'Medium',
severity: testCase.severity || 'Minor',
environment: testCase.environment || 'Staging',
stepsToReproduce: testCase.stepsToReproduce || '',
inputs: testCase.inputs || '',
expectedBehavior: testCase.expectedBehavior || '',
actualBehavior: testCase.actualBehavior || '',
rootCause: testCase.rootCause || '',
resolution: testCase.resolution || '',
version: testCase.version || '',
url: testCase.url || '',
};
await tasksApi.create(taskData);
setSavingStates(prev => ({ ...prev, [index]: 'saved' }));
toast.success(`Saved: ${testCase.title}`);
} catch (error) {
console.error('Failed to save test case:', error);
setSavingStates(prev => ({ ...prev, [index]: 'error' }));
toast.error(`Failed to save: ${testCase.title}`);
}
};
// Save all selected test cases
const saveSelected = async () => {
if (selectedCases.size === 0) {
toast.warning('No test cases selected');
return;
}
setIsSavingAll(true);
const indices = Array.from(selectedCases);
for (const index of indices) {
if (savingStates[index] !== 'saved') {
await saveTestCase(index);
}
}
setIsSavingAll(false);
onSaveComplete();
// Check if all saved successfully
const savedCount = indices.filter(i => savingStates[i] === 'saved').length;
if (savedCount === indices.length) {
toast.success(`All ${savedCount} test cases saved successfully!`);
onOpenChange(false);
}
};
// Get priority badge color
const getPriorityColor = (priority: string) => {
switch (priority?.toLowerCase()) {
case 'critical': return 'bg-red-100 text-red-800 border-red-300';
case 'high': return 'bg-orange-100 text-orange-800 border-orange-300';
case 'medium': return 'bg-yellow-100 text-yellow-800 border-yellow-300';
case 'low': return 'bg-green-100 text-green-800 border-green-300';
default: return 'bg-gray-100 text-gray-800 border-gray-300';
}
};
// Get severity badge color
const getSeverityColor = (severity: string) => {
switch (severity?.toLowerCase()) {
case 'blocker': return 'bg-red-100 text-red-800 border-red-300';
case 'critical': return 'bg-red-100 text-red-800 border-red-300';
case 'major': return 'bg-orange-100 text-orange-800 border-orange-300';
case 'minor': return 'bg-yellow-100 text-yellow-800 border-yellow-300';
case 'trivial': return 'bg-green-100 text-green-800 border-green-300';
default: return 'bg-gray-100 text-gray-800 border-gray-300';
}
};
// Reset state when dialog opens
React.useEffect(() => {
if (open) {
setSelectedCases(new Set(testCases.map((_, i) => i)));
setSavingStates({});
setExpandedCases(new Set([0]));
setEditedCases({});
}
}, [open, testCases]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-purple-500" />
Generated Test Cases Preview
</DialogTitle>
<DialogDescription>
Review the AI-generated test cases below. Select which ones to save to the database.
You can edit any field before saving.
</DialogDescription>
</DialogHeader>
<div className="flex items-center justify-between py-2 border-b">
<div className="flex items-center gap-2">
<Checkbox
id="select-all"
checked={selectedCases.size === testCases.length}
onCheckedChange={toggleSelectAll}
/>
<label htmlFor="select-all" className="text-sm font-medium cursor-pointer">
Select All ({selectedCases.size}/{testCases.length} selected)
</label>
</div>
<div className="text-sm text-muted-foreground">
Feature ID: {featureId}
</div>
</div>
<div className="flex-1 overflow-y-auto space-y-3 py-4 pr-2">
{testCases.map((tc, index) => {
const currentCase = editedCases[index] || tc;
const isExpanded = expandedCases.has(index);
const isSelected = selectedCases.has(index);
const saveState = savingStates[index] || 'idle';
return (
<div
key={index}
className={`border rounded-lg transition-all ${
isSelected ? 'border-purple-300 bg-purple-50/30' : 'border-gray-200'
}`}
>
{/* Header */}
<div className="flex items-start gap-3 p-3">
<Checkbox
checked={isSelected}
onCheckedChange={() => toggleSelection(index)}
className="mt-1"
/>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<input
type="text"
value={currentCase.title}
onChange={(e) => updateTestCase(index, 'title', e.target.value)}
className="font-medium text-sm w-full bg-transparent border-b border-transparent hover:border-gray-300 focus:border-purple-500 focus:outline-none px-0.5"
/>
<div className="flex flex-wrap items-center gap-1.5 mt-1.5">
<Badge variant="outline" className={getPriorityColor(currentCase.priority)}>
{currentCase.priority}
</Badge>
<Badge variant="outline" className={getSeverityColor(currentCase.severity)}>
{currentCase.severity}
</Badge>
<Badge variant="outline" className="bg-blue-50 text-blue-800 border-blue-200">
{currentCase.environment}
</Badge>
{saveState === 'saved' && (
<Badge className="bg-green-500 text-white">
<CheckCircle className="h-3 w-3 mr-1" />
Saved
</Badge>
)}
{saveState === 'error' && (
<Badge className="bg-red-500 text-white">
<XCircle className="h-3 w-3 mr-1" />
Error
</Badge>
)}
</div>
</div>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => saveTestCase(index)}
disabled={saveState === 'saving' || saveState === 'saved'}
className="h-8"
>
{saveState === 'saving' ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Save className="h-4 w-4" />
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => toggleExpand(index)}
className="h-8"
>
{isExpanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
</div>
</div>
</div>
</div>
{/* Expanded Content */}
{isExpanded && (
<div className="px-3 pb-3 pt-0 space-y-3 border-t">
{/* Description */}
<div className="pt-3">
<label className="text-xs font-medium text-muted-foreground">Description</label>
<textarea
value={currentCase.description}
onChange={(e) => updateTestCase(index, 'description', e.target.value)}
className="w-full mt-1 text-sm border rounded-md p-2 min-h-[60px] focus:outline-none focus:ring-1 focus:ring-purple-500"
/>
</div>
{/* Steps to Reproduce */}
<div>
<label className="text-xs font-medium text-muted-foreground">Steps to Reproduce</label>
<textarea
value={currentCase.stepsToReproduce || ''}
onChange={(e) => updateTestCase(index, 'stepsToReproduce', e.target.value)}
className="w-full mt-1 text-sm border rounded-md p-2 min-h-[80px] font-mono focus:outline-none focus:ring-1 focus:ring-purple-500"
placeholder="1. First step&#10;2. Second step&#10;3. Third step"
/>
</div>
{/* Test Data / Inputs */}
<div>
<label className="text-xs font-medium text-muted-foreground">Test Data / Inputs</label>
<textarea
value={currentCase.inputs || ''}
onChange={(e) => updateTestCase(index, 'inputs', e.target.value)}
className="w-full mt-1 text-sm border rounded-md p-2 min-h-[40px] focus:outline-none focus:ring-1 focus:ring-purple-500"
/>
</div>
{/* Expected Behavior */}
<div>
<label className="text-xs font-medium text-muted-foreground">Expected Behavior</label>
<textarea
value={currentCase.expectedBehavior || ''}
onChange={(e) => updateTestCase(index, 'expectedBehavior', e.target.value)}
className="w-full mt-1 text-sm border rounded-md p-2 min-h-[40px] focus:outline-none focus:ring-1 focus:ring-purple-500"
/>
</div>
{/* Two column layout for smaller fields */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-muted-foreground">Priority</label>
<select
value={currentCase.priority || 'Medium'}
onChange={(e) => updateTestCase(index, 'priority', e.target.value)}
className="w-full mt-1 text-sm border rounded-md p-2 focus:outline-none focus:ring-1 focus:ring-purple-500"
>
<option value="Critical">Critical</option>
<option value="High">High</option>
<option value="Medium">Medium</option>
<option value="Low">Low</option>
</select>
</div>
<div>
<label className="text-xs font-medium text-muted-foreground">Severity</label>
<select
value={currentCase.severity || 'Minor'}
onChange={(e) => updateTestCase(index, 'severity', e.target.value)}
className="w-full mt-1 text-sm border rounded-md p-2 focus:outline-none focus:ring-1 focus:ring-purple-500"
>
<option value="Blocker">Blocker</option>
<option value="Critical">Critical</option>
<option value="Major">Major</option>
<option value="Minor">Minor</option>
<option value="Trivial">Trivial</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-muted-foreground">Environment</label>
<select
value={currentCase.environment || 'Staging'}
onChange={(e) => updateTestCase(index, 'environment', e.target.value)}
className="w-full mt-1 text-sm border rounded-md p-2 focus:outline-none focus:ring-1 focus:ring-purple-500"
>
<option value="Development">Development</option>
<option value="Testing">Testing</option>
<option value="Staging">Staging</option>
<option value="Production">Production</option>
</select>
</div>
<div>
<label className="text-xs font-medium text-muted-foreground">Version</label>
<input
type="text"
value={currentCase.version || ''}
onChange={(e) => updateTestCase(index, 'version', e.target.value)}
className="w-full mt-1 text-sm border rounded-md p-2 focus:outline-none focus:ring-1 focus:ring-purple-500"
placeholder="e.g., 1.2.3"
/>
</div>
</div>
</div>
)}
</div>
);
})}
</div>
<DialogFooter className="border-t pt-4">
<div className="flex items-center justify-between w-full">
<div className="text-sm text-muted-foreground">
{selectedCases.size} test case(s) selected
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
onClick={saveSelected}
disabled={selectedCases.size === 0 || isSavingAll}
className="bg-purple-600 hover:bg-purple-700"
>
{isSavingAll ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Saving...
</>
) : (
<>
<Save className="h-4 w-4 mr-2" />
Save Selected ({selectedCases.size})
</>
)}
</Button>
</div>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default GeneratedTestCasesPreview;