diff --git a/frontend/src/lib/services/evaluators.ts b/frontend/src/lib/services/evaluators.ts index d182ceb..b52e6a6 100644 --- a/frontend/src/lib/services/evaluators.ts +++ b/frontend/src/lib/services/evaluators.ts @@ -1,4 +1,4 @@ -import { checkKeyText, validateNodes } from '$services/exercise'; +import { checkKeyText, missingKeyTextKeys, validateNodes } from '$services/exercise'; import type { LessonContent } from '$types/lesson'; import type { CircuitJSApi } from '$types/circuitjs'; @@ -52,6 +52,12 @@ export function evaluateCircuitSubmission( return { pass: allPassed, output: finalOutput }; } +export interface LanguageEvalResult { + isCorrect: boolean; + reason?: 'output-mismatch' | 'keytext-missing'; + missingKeys?: string[]; +} + export function processLanguageEvaluation( compileOutput: string, code: string, @@ -60,7 +66,7 @@ export function processLanguageEvaluation( cCode: string, pythonCode: string, data: LessonContent -): { isCorrect: boolean } { +): LanguageEvalResult { if (!data.expected_output && !data.expected_output_python) return { isCorrect: true }; const currentCCode = (currentLanguage === 'c') ? code : cCode; @@ -71,6 +77,15 @@ export function processLanguageEvaluation( ? data.expected_output_python : data.expected_output; - const isCorrect = compileOutput.trim() === expected.trim() && checkKeyText(mergedCode, data.key_text ?? ''); - return { isCorrect }; + const outputMatches = compileOutput.trim() === (expected ?? '').trim(); + if (!outputMatches) { + return { isCorrect: false, reason: 'output-mismatch' }; + } + + const missingKeys = missingKeyTextKeys(mergedCode, data.key_text ?? ''); + if (missingKeys.length > 0) { + return { isCorrect: false, reason: 'keytext-missing', missingKeys }; + } + + return { isCorrect: true }; } diff --git a/frontend/src/lib/services/exercise.test.ts b/frontend/src/lib/services/exercise.test.ts new file mode 100644 index 0000000..8230b3d --- /dev/null +++ b/frontend/src/lib/services/exercise.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; +import { checkKeyText, missingKeyTextKeys } from './exercise'; +import { processLanguageEvaluation } from './evaluators'; +import type { LessonContent } from '$types/lesson'; + +describe('missingKeyTextKeys', () => { + it('mengembalikan key yang hilang (bukan hanya boolean)', () => { + expect(missingKeyTextKeys('int main() {}', 'int\nfloat\n%.2lf')).toEqual(['float', '%.2lf']); + }); + + it('kompatibel dengan checkKeyText', () => { + expect(checkKeyText('int x; float y;', 'int\nfloat')).toBe(true); + expect(checkKeyText('int x;', 'int\nfloat')).toBe(false); + }); + + it('key_text kosong tidak menghasilkan key hilang', () => { + expect(missingKeyTextKeys('apa saja', ' \n')).toEqual([]); + }); +}); + +describe('processLanguageEvaluation feedback', () => { + const data = { + expected_output: 'Halo', + key_text: 'printf\n%.2f', + } as unknown as LessonContent; + + it('output beda → reason output-mismatch', () => { + const result = processLanguageEvaluation('Halo dunia', 'printf("%.2f", 1);', 'c', 'c', '', '', data); + expect(result.isCorrect).toBe(false); + expect(result.reason).toBe('output-mismatch'); + }); + + it('output sama tapi key_text kurang → reason keytext-missing + daftar key', () => { + const result = processLanguageEvaluation('Halo', 'printf("Halo");', 'c', 'c', '', '', data); + expect(result.isCorrect).toBe(false); + expect(result.reason).toBe('keytext-missing'); + expect(result.missingKeys).toEqual(['%.2f']); + }); + + it('output sama dan key lengkap → correct tanpa reason', () => { + const result = processLanguageEvaluation('Halo', 'printf("%.2f", 1);', 'c', 'c', '', '', data); + expect(result).toEqual({ isCorrect: true }); + }); +}); diff --git a/frontend/src/lib/services/exercise.ts b/frontend/src/lib/services/exercise.ts index 7ff83fe..ebec854 100644 --- a/frontend/src/lib/services/exercise.ts +++ b/frontend/src/lib/services/exercise.ts @@ -4,11 +4,16 @@ import type { CircuitJSApi } from '$types/circuitjs'; +/** Return key_text keywords (one per line) that are missing from the code. */ +export function missingKeyTextKeys(code: string, keyText: string): string[] { + if (!keyText.trim()) return []; + const keys = keyText.split('\n').map(k => k.trim()).filter(k => k.length > 0); + return keys.filter(key => !code.includes(key)); +} + /** Check if code contains all required key_text keywords (one per line). */ export function checkKeyText(code: string, keyText: string): boolean { - if (!keyText.trim()) return true; - const keys = keyText.split('\n').map(k => k.trim()).filter(k => k.length > 0); - return keys.every(key => code.includes(key)); + return missingKeyTextKeys(code, keyText).length === 0; } export interface NodeResult { diff --git a/frontend/src/routes/lesson/[slug]/lesson.svelte.ts b/frontend/src/routes/lesson/[slug]/lesson.svelte.ts index 2ba8982..b09336f 100644 --- a/frontend/src/routes/lesson/[slug]/lesson.svelte.ts +++ b/frontend/src/routes/lesson/[slug]/lesson.svelte.ts @@ -865,8 +865,8 @@ export class LessonManager { out.output = res.output; out.success = true; if (this.data.expected_output || this.data.expected_output_python) { - const { isCorrect } = processLanguageEvaluation(res.output, code, lang, this.currentLanguage, this.cCode, this.pythonCode, this.data); - if (isCorrect) { + const evaluation = processLanguageEvaluation(res.output, code, lang, this.currentLanguage, this.cCode, this.pythonCode, this.data); + if (evaluation.isCorrect) { if (lang === 'c') this.cPassed = true; else if (lang === 'python') this.pythonPassed = true; if (!this.checkAllPassed()) { @@ -883,6 +883,14 @@ export class LessonManager { } setTimeout(() => { this.showCelebration = false; this.activeTab = 'editor'; }, 3000); } + } else { + out.success = false; + if (evaluation.reason === 'keytext-missing') { + const missing = (evaluation.missingKeys ?? []).map(k => `\`${k.replace(/\n/g, '\\n')}\``).join(', '); + out.output += `\n❌ Output sudah sesuai, tapi kode belum memakai konsep materi. Kata kunci wajib yang belum ada: ${missing}`; + } else if (evaluation.reason === 'output-mismatch') { + out.output += '\n❌ Output belum sesuai yang diharapkan — periksa format keluaran programmu.'; + } } } } catch (err: any) {