From 52659b80d711a7f2b64c5b9c6386fdce56d301ca Mon Sep 17 00:00:00 2001 From: vstudio79 Date: Thu, 30 Jul 2026 17:59:22 +0100 Subject: [PATCH] test(useQuiz): add navigation bounds and review-mode guard tests Add tests for: - goPrevious clamping at index 0 when already at first question - goNext clamping at questions.length - 1 when already at last question - canGoNext/canGoPrevious boundary state reflection - enterReviewMode no-op before completion - exitReviewMode no-op before completion - enterReviewMode sets review mode after completion - exitReviewMode clears review mode after completion Closes #948 --- src/hooks/__tests__/useQuiz.test.tsx | 128 +++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/src/hooks/__tests__/useQuiz.test.tsx b/src/hooks/__tests__/useQuiz.test.tsx index 5ea706e6..81c03958 100644 --- a/src/hooks/__tests__/useQuiz.test.tsx +++ b/src/hooks/__tests__/useQuiz.test.tsx @@ -120,4 +120,132 @@ describe('useQuiz', () => { partialCreditEnabled: true, }); }); + + describe('navigation bounds', () => { + it('goPrevious at the first question clamps to index 0', () => { + const { result } = renderHook(() => useQuiz({ quiz: quizFixture, autoStart: false })); + + expect(result.current.currentQuestionIndex).toBe(0); + + act(() => { + result.current.actions.goPrevious(); + }); + + expect(result.current.currentQuestionIndex).toBe(0); + }); + + it('goNext advances and goPrevious goes back within bounds', () => { + const { result } = renderHook(() => useQuiz({ quiz: quizFixture, autoStart: false })); + + act(() => { + result.current.actions.goNext(); + }); + + expect(result.current.currentQuestionIndex).toBe(1); + + act(() => { + result.current.actions.goPrevious(); + }); + + expect(result.current.currentQuestionIndex).toBe(0); + }); + + it('goNext at the last question clamps to questions.length - 1', () => { + const multiQuestionFixture: Quiz = { + ...quizFixture, + questions: [ + ...quizFixture.questions, + { + id: 'mc-2', + type: 'multiple-choice', + text: 'Third question', + points: 1, + options: [ + { id: 'a', text: 'Wrong', isCorrect: false }, + { id: 'b', text: 'Right', isCorrect: true }, + ], + }, + ], + }; + + const { result } = renderHook(() => useQuiz({ quiz: multiQuestionFixture, autoStart: false })); + + act(() => { + result.current.actions.goNext(); + }); + act(() => { + result.current.actions.goNext(); + }); + + expect(result.current.currentQuestionIndex).toBe(2); + + act(() => { + result.current.actions.goNext(); + }); + + expect(result.current.currentQuestionIndex).toBe(2); + }); + + it('canGoNext and canGoPrevious reflect boundary state', () => { + const { result } = renderHook(() => useQuiz({ quiz: quizFixture, autoStart: false })); + + expect(result.current.canGoPrevious).toBe(false); + expect(result.current.canGoNext).toBe(true); + + act(() => { + result.current.actions.goNext(); + }); + + expect(result.current.canGoPrevious).toBe(true); + expect(result.current.canGoNext).toBe(false); + }); + }); + + describe('review-mode guards', () => { + it('enterReviewMode is a no-op before the quiz is completed', () => { + const { result } = renderHook(() => useQuiz({ quiz: quizFixture, autoStart: false })); + + act(() => { + result.current.actions.enterReviewMode(); + }); + + expect(result.current.isReviewMode).toBe(false); + }); + + it('exitReviewMode is a no-op before the quiz is completed', () => { + const { result } = renderHook(() => useQuiz({ quiz: quizFixture, autoStart: false })); + + act(() => { + result.current.actions.exitReviewMode(); + }); + + expect(result.current.isReviewMode).toBe(false); + }); + + it('enterReviewMode sets isReviewMode after completion', () => { + const { result } = renderHook(() => useQuiz({ quiz: quizFixture, autoStart: false })); + + act(() => { + result.current.actions.complete(); + }); + + expect(result.current.isReviewMode).toBe(true); + }); + + it('exitReviewMode clears isReviewMode after completion', () => { + const { result } = renderHook(() => useQuiz({ quiz: quizFixture, autoStart: false })); + + act(() => { + result.current.actions.complete(); + }); + + expect(result.current.isReviewMode).toBe(true); + + act(() => { + result.current.actions.exitReviewMode(); + }); + + expect(result.current.isReviewMode).toBe(false); + }); + }); });