Download src/components/Practice/TestRunner.tsx from samuellimabraz/quantum-assistant: direct link, hf CLI and curl.
- Browser
- Download file 9.96 kB
-
https://huggingface.co/spaces/samuellimabraz/quantum-assistant/resolve/main/src/components/Practice/TestRunner.tsx
- Command line
-
hf download hf://spaces/samuellimabraz/quantum-assistant/src/components/Practice/TestRunner.tsx
-
curl -L -o TestRunner.tsx https://huggingface.co/spaces/samuellimabraz/quantum-assistant/resolve/main/src/components/Practice/TestRunner.tsx
9.96 kB
| 'use client'; | |
| import { useState } from 'react'; | |
| import { | |
| Play, | |
| CheckCircle2, | |
| XCircle, | |
| Clock, | |
| ChevronDown, | |
| ChevronUp, | |
| AlertTriangle, | |
| Loader2, | |
| Terminal, | |
| FileCode, | |
| } from 'lucide-react'; | |
| import { clsx } from 'clsx'; | |
| import type { TestResult } from '@/types'; | |
| interface TestRunnerProps { | |
| userCode: string; | |
| testCode: string; | |
| entryPoint: string; | |
| onTestComplete: (result: TestResult) => void; | |
| initialResult?: TestResult | null; | |
| } | |
| export function TestRunner({ | |
| userCode, | |
| testCode, | |
| entryPoint, | |
| onTestComplete, | |
| initialResult = null, | |
| }: TestRunnerProps) { | |
| const [isRunning, setIsRunning] = useState(false); | |
| const [result, setResult] = useState<TestResult | null>(initialResult); | |
| const [isExpanded, setIsExpanded] = useState(false); | |
| const [showTraceback, setShowTraceback] = useState(false); | |
| const runTests = async () => { | |
| if (isRunning) return; | |
| setIsRunning(true); | |
| setResult(null); | |
| setShowTraceback(false); | |
| try { | |
| const response = await fetch('/api/test', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| userCode, | |
| testCode, | |
| entryPoint, | |
| timeout: 30, | |
| }), | |
| }); | |
| const data = await response.json(); | |
| setResult(data); | |
| onTestComplete(data); | |
| // Auto-expand on failure | |
| if (!data.passed) { | |
| setIsExpanded(true); | |
| } | |
| } catch (error) { | |
| const errorResult: TestResult = { | |
| passed: false, | |
| total: 0, | |
| failed: 0, | |
| details: [], | |
| executionTime: 0, | |
| error: error instanceof Error ? error.message : 'Failed to run tests', | |
| }; | |
| setResult(errorResult); | |
| onTestComplete(errorResult); | |
| setIsExpanded(true); | |
| } finally { | |
| setIsRunning(false); | |
| } | |
| }; | |
| const hasDetails = result && (result.error || result.details.length > 0 || result.traceback || result.output); | |
| const passedCount = result?.details?.filter(t => t.passed).length ?? 0; | |
| const totalCount = result?.total ?? result?.details?.length ?? 0; | |
| return ( | |
| <div className="flex flex-col"> | |
| {/* Compact header with Run button and status */} | |
| <div className="flex items-center justify-between px-4 py-2 bg-zinc-900/50"> | |
| <div className="flex items-center gap-3"> | |
| <button | |
| onClick={runTests} | |
| disabled={isRunning || !userCode.trim()} | |
| className={clsx( | |
| 'flex items-center gap-2 px-3 py-1.5 rounded-md font-medium text-sm transition-all', | |
| isRunning || !userCode.trim() | |
| ? 'bg-zinc-800 text-zinc-500 cursor-not-allowed' | |
| : 'bg-teal-600 hover:bg-teal-500 text-white' | |
| )} | |
| > | |
| {isRunning ? ( | |
| <> | |
| <Loader2 className="w-3.5 h-3.5 animate-spin" /> | |
| Running... | |
| </> | |
| ) : ( | |
| <> | |
| <Play className="w-3.5 h-3.5" /> | |
| Run Tests | |
| </> | |
| )} | |
| </button> | |
| {result && ( | |
| <div className="flex items-center gap-2 text-sm"> | |
| {result.passed ? ( | |
| <span className="flex items-center gap-1.5 text-emerald-400"> | |
| <CheckCircle2 className="w-4 h-4" /> | |
| <span>Passed</span> | |
| {totalCount > 0 && ( | |
| <span className="text-emerald-400/70 text-xs font-normal"> | |
| ({passedCount}/{totalCount} tests) | |
| </span> | |
| )} | |
| </span> | |
| ) : ( | |
| <span className="flex items-center gap-1.5 text-red-400"> | |
| <XCircle className="w-4 h-4" /> | |
| <span>Failed</span> | |
| {totalCount > 0 && ( | |
| <span className="text-red-400/70 text-xs font-normal"> | |
| ({passedCount}/{totalCount} tests) | |
| </span> | |
| )} | |
| </span> | |
| )} | |
| <span className="flex items-center gap-1 text-zinc-500 text-xs"> | |
| <Clock className="w-3 h-3" /> | |
| {result.executionTime}ms | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| {result && ( | |
| <button | |
| onClick={() => setIsExpanded(!isExpanded)} | |
| className="flex items-center gap-1 text-xs text-zinc-500 hover:text-zinc-300 transition-colors" | |
| > | |
| {isExpanded ? 'Hide' : 'Show'} details | |
| {isExpanded ? ( | |
| <ChevronUp className="w-3.5 h-3.5" /> | |
| ) : ( | |
| <ChevronDown className="w-3.5 h-3.5" /> | |
| )} | |
| </button> | |
| )} | |
| </div> | |
| {/* Expandable details section */} | |
| {isExpanded && result && ( | |
| <div className="px-4 py-3 bg-zinc-900/30 border-t border-zinc-800/50 max-h-64 overflow-y-auto"> | |
| {/* Summary for passed tests */} | |
| {result.passed && !result.error && ( | |
| <div className="p-3 rounded-lg bg-emerald-950/20 border border-emerald-800/30 mb-3"> | |
| <div className="flex items-center gap-2"> | |
| <CheckCircle2 className="w-4 h-4 text-emerald-400" /> | |
| <span className="text-sm text-emerald-300 font-medium"> | |
| All {totalCount} test{totalCount !== 1 ? 's' : ''} passed successfully! | |
| </span> | |
| </div> | |
| </div> | |
| )} | |
| {/* Main error message */} | |
| {result.error && ( | |
| <div className="p-3 rounded-lg bg-red-950/30 border border-red-800/40 mb-3"> | |
| <div className="flex items-start gap-2"> | |
| <AlertTriangle className="w-4 h-4 text-red-400 flex-shrink-0 mt-0.5" /> | |
| <pre className="text-xs text-red-200/80 whitespace-pre-wrap font-mono flex-1 break-all"> | |
| {result.error} | |
| </pre> | |
| </div> | |
| </div> | |
| )} | |
| {/* Traceback toggle and display */} | |
| {result.traceback && result.traceback !== result.error && ( | |
| <div className="mb-3"> | |
| <button | |
| onClick={() => setShowTraceback(!showTraceback)} | |
| className="flex items-center gap-2 text-xs text-zinc-400 hover:text-zinc-200 transition-colors mb-2" | |
| > | |
| <FileCode className="w-3.5 h-3.5" /> | |
| {showTraceback ? 'Hide' : 'Show'} full traceback | |
| {showTraceback ? ( | |
| <ChevronUp className="w-3 h-3" /> | |
| ) : ( | |
| <ChevronDown className="w-3 h-3" /> | |
| )} | |
| </button> | |
| {showTraceback && ( | |
| <div className="p-3 rounded-lg bg-zinc-900/80 border border-zinc-700/50 overflow-x-auto"> | |
| <pre className="text-[11px] text-zinc-300 whitespace-pre font-mono"> | |
| {result.traceback} | |
| </pre> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* Output display */} | |
| {result.output && ( | |
| <div className="mb-3"> | |
| <div className="flex items-center gap-2 text-xs text-zinc-500 mb-1.5"> | |
| <Terminal className="w-3.5 h-3.5" /> | |
| <span>Output</span> | |
| </div> | |
| <div className="p-2 rounded-lg bg-zinc-900/60 border border-zinc-800/50"> | |
| <pre className="text-[11px] text-zinc-400 whitespace-pre-wrap font-mono"> | |
| {result.output} | |
| </pre> | |
| </div> | |
| </div> | |
| )} | |
| {/* Test details - always show */} | |
| {result.details && result.details.length > 0 && ( | |
| <div className="space-y-2"> | |
| <div className="text-xs text-zinc-500 mb-2">Test Results:</div> | |
| {result.details.map((test, idx) => ( | |
| <div | |
| key={idx} | |
| className={clsx( | |
| 'p-2 rounded-md border text-xs', | |
| test.passed | |
| ? 'bg-emerald-950/20 border-emerald-800/30' | |
| : 'bg-red-950/20 border-red-800/30' | |
| )} | |
| > | |
| <div className="flex items-center gap-2"> | |
| {test.passed ? ( | |
| <CheckCircle2 className="w-3.5 h-3.5 text-emerald-400" /> | |
| ) : ( | |
| <XCircle className="w-3.5 h-3.5 text-red-400" /> | |
| )} | |
| <span | |
| className={clsx( | |
| 'font-medium', | |
| test.passed ? 'text-emerald-300' : 'text-red-300' | |
| )} | |
| > | |
| {test.name} | |
| </span> | |
| </div> | |
| {!test.passed && (test.expected || test.actual || test.error) && ( | |
| <div className="ml-5 mt-1 space-y-0.5 font-mono text-[11px]"> | |
| {test.expected && ( | |
| <p className="text-zinc-400"> | |
| <span className="text-zinc-500">Expected: </span> | |
| <span className="text-emerald-300">{test.expected}</span> | |
| </p> | |
| )} | |
| {test.actual && ( | |
| <p className="text-zinc-400"> | |
| <span className="text-zinc-500">Actual: </span> | |
| <span className="text-red-300">{test.actual}</span> | |
| </p> | |
| )} | |
| {test.error && !result.error && ( | |
| <p className="text-red-300/80">{test.error}</p> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |