11'use client' ;
22
33import { useMemo , useState } from 'react' ;
4- import type { CodeChallengeQuizQuestion , UseQuizReturn } from '@/hooks/useQuiz' ;
4+ import {
5+ normalizeQuizOutput ,
6+ type CodeChallengeQuizQuestion ,
7+ type UseQuizReturn ,
8+ } from '@/hooks/useQuiz' ;
59
610interface CodeChallengeQuestionProps {
711 question : CodeChallengeQuizQuestion ;
812 quizState : UseQuizReturn ;
913}
1014
11- function normalizeOutput ( value : unknown ) {
12- if ( typeof value === 'string' ) return value ;
13- if ( typeof value === 'number' || typeof value === 'boolean' ) return String ( value ) ;
14- try {
15- return JSON . stringify ( value ) ;
16- } catch {
17- return String ( value ) ;
18- }
19- }
20-
2115export default function CodeChallengeQuestion ( { question, quizState } : CodeChallengeQuestionProps ) {
2216 const { answers, isReviewMode, isCompleted, actions } = quizState ;
2317 const existing = answers [ question . id ] ;
@@ -29,30 +23,44 @@ export default function CodeChallengeQuestion({ question, quizState }: CodeChall
2923 const [ isRunning , setIsRunning ] = useState ( false ) ;
3024
3125 const hasTestCases = Boolean ( question . testCases && question . testCases . length ) ;
26+ const passedTests = testResults . filter ( Boolean ) . length ;
27+ const totalTests = testResults . length ;
3228
3329 const overallPassed = useMemo (
3430 ( ) => ( testResults . length ? testResults . every ( Boolean ) : false ) ,
3531 [ testResults ] ,
3632 ) ;
33+ const partialPass =
34+ totalTests > 0 && passedTests > 0 && ! overallPassed && Boolean ( question . gradingPolicy ?. partialCredit ) ;
3735
3836 const runTests = ( ) => {
3937 if ( ! hasTestCases || ! question . testCases ) return ;
4038
4139 setIsRunning ( true ) ;
4240
43- const results = question . testCases . map ( ( testCase ) => {
44- try {
45- const userFunction = new Function ( 'input' , code ) ;
46- const output = userFunction ( testCase . input ) ;
47- return normalizeOutput ( output ) === normalizeOutput ( testCase . expectedOutput ) ;
48- } catch {
49- return false ;
50- }
51- } ) ;
52-
53- setTestResults ( results ) ;
54- actions . setCodeChallengeResult ( question . id , { code, testResults : results } ) ;
55- setIsRunning ( false ) ;
41+ try {
42+ const userFunction = new Function ( 'input' , code ) as ( input : string ) => unknown ;
43+ const results = question . testCases . map ( ( testCase ) => {
44+ try {
45+ const output = userFunction ( testCase . input ) ;
46+ return (
47+ normalizeQuizOutput ( output , question . gradingPolicy ) ===
48+ normalizeQuizOutput ( testCase . expectedOutput , question . gradingPolicy )
49+ ) ;
50+ } catch {
51+ return false ;
52+ }
53+ } ) ;
54+
55+ setTestResults ( results ) ;
56+ actions . setCodeChallengeResult ( question . id , { code, testResults : results } ) ;
57+ } catch {
58+ const results = question . testCases . map ( ( ) => false ) ;
59+ setTestResults ( results ) ;
60+ actions . setCodeChallengeResult ( question . id , { code, testResults : results } ) ;
61+ } finally {
62+ setIsRunning ( false ) ;
63+ }
5664 } ;
5765
5866 const onChangeCode = ( value : string ) => {
@@ -85,19 +93,36 @@ export default function CodeChallengeQuestion({ question, quizState }: CodeChall
8593 < div className = "space-y-2" >
8694 < div
8795 className = { `text-sm font-medium ${
88- overallPassed ? 'text-[#0066FF] dark:text-[#00C2FF]' : 'text-red-700'
96+ overallPassed
97+ ? 'text-[#0066FF] dark:text-[#00C2FF]'
98+ : partialPass
99+ ? 'text-amber-700 dark:text-amber-300'
100+ : 'text-red-700'
89101 } `}
90102 >
91- { overallPassed ? 'All tests passed' : 'Some tests failed' }
103+ { overallPassed
104+ ? 'All tests passed'
105+ : partialPass
106+ ? `${ passedTests } of ${ totalTests } tests passed`
107+ : 'Some tests failed' }
92108 </ div >
93109
110+ { partialPass && question . gradingPolicy ?. partialCredit ? (
111+ < div className = "rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-200" >
112+ This submission is partially correct. The grader will tolerate the failed tests and
113+ award partial credit.
114+ </ div >
115+ ) : null }
116+
94117 { question . testCases . map ( ( testCase , index ) => (
95118 < div
96119 key = { index }
97120 className = { `p-3 rounded-lg border ${
98121 testResults [ index ]
99122 ? 'bg-[#F0F9FF] dark:bg-[#1E3A8A]/20 border-[#0066FF]/20 dark:border-[#00C2FF]/20'
100- : 'bg-red-50 border-red-200'
123+ : partialPass
124+ ? 'bg-amber-50 border-amber-200 dark:bg-amber-950/40 dark:border-amber-700'
125+ : 'bg-red-50 border-red-200'
101126 } `}
102127 >
103128 < div className = "flex items-start justify-between gap-4" >
@@ -114,7 +139,11 @@ export default function CodeChallengeQuestion({ question, quizState }: CodeChall
114139 </ div >
115140 < div
116141 className = { `text-sm font-medium ${
117- testResults [ index ] ? 'text-[#0066FF] dark:text-[#00C2FF]' : 'text-red-700'
142+ testResults [ index ]
143+ ? 'text-[#0066FF] dark:text-[#00C2FF]'
144+ : partialPass
145+ ? 'text-amber-700 dark:text-amber-300'
146+ : 'text-red-700'
118147 } `}
119148 >
120149 { testResults [ index ] ? 'Pass' : 'Fail' }
0 commit comments