Skip to content

Commit 3712c71

Browse files
committed
feat(store): add initial tests for quizSlice
1 parent 7c37490 commit 3712c71

File tree

2 files changed

+315
-42
lines changed

2 files changed

+315
-42
lines changed

app/store/quizSlice.test.ts

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { create } from 'zustand';
3+
import { immer } from 'zustand/middleware/immer';
4+
import type { TextGeneratorState } from './textGeneratorStore';
5+
import { createQuizSlice, INITIAL_HOVER_CREDITS } from './quizSlice';
6+
import type { BaseSlice } from './baseSlice';
7+
import type { UISlice } from './uiSlice';
8+
import type { SettingsSlice } from './settingsSlice';
9+
import type { PartialQuizData } from '@/lib/domain/schemas';
10+
import { generateExerciseResponse } from '@/app/actions/exercise';
11+
import { submitAnswer, submitQuestionFeedback } from '@/app/actions/userProgress';
12+
13+
vi.mock('@/app/actions/userProgress', () => ({
14+
submitAnswer: vi.fn(),
15+
submitQuestionFeedback: vi.fn(),
16+
}));
17+
vi.mock('@/app/actions/exercise', () => ({
18+
generateExerciseResponse: vi.fn(),
19+
}));
20+
21+
const mockBaseSlice: BaseSlice = {
22+
loading: false,
23+
error: null,
24+
showError: false,
25+
setLoading: vi.fn(),
26+
setError: vi.fn(),
27+
};
28+
29+
const mockUISlice: UISlice = {
30+
...mockBaseSlice,
31+
showLoginPrompt: false,
32+
showContent: true,
33+
showQuestionSection: false,
34+
showExplanation: false,
35+
setShowLoginPrompt: vi.fn(),
36+
setShowContent: vi.fn(),
37+
setShowQuestionSection: vi.fn(),
38+
setShowExplanation: vi.fn(),
39+
};
40+
41+
const mockSettingsSlice: SettingsSlice = {
42+
...mockBaseSlice,
43+
passageLanguage: 'en',
44+
generatedPassageLanguage: null,
45+
generatedQuestionLanguage: 'en',
46+
cefrLevel: 'A1',
47+
setPassageLanguage: vi.fn(),
48+
setGeneratedPassageLanguage: vi.fn(),
49+
setGeneratedQuestionLanguage: vi.fn(),
50+
setCefrLevel: vi.fn(),
51+
};
52+
53+
const mockOtherStateAndFunctions = {
54+
isSpeechSupported: false,
55+
isSpeakingPassage: false,
56+
isPaused: false,
57+
volume: 1,
58+
rate: 1,
59+
currentWordIndex: null,
60+
currentSentenceIndex: null,
61+
voices: [],
62+
selectedVoiceURI: null,
63+
setVolume: vi.fn(),
64+
setRate: vi.fn(),
65+
setIsPaused: vi.fn(),
66+
setSelectedVoiceURI: vi.fn(),
67+
updateAvailableVoices: vi.fn(),
68+
speakPassage: vi.fn(),
69+
pauseSpeech: vi.fn(),
70+
resumeSpeech: vi.fn(),
71+
stopPassageSpeech: vi.fn(),
72+
isSpeaking: false,
73+
userStreak: 0,
74+
setUserStreak: vi.fn(),
75+
fetchUserProgress: vi.fn(),
76+
textSettings: {
77+
isPassageVisible: true,
78+
fontScale: 1,
79+
highlightWords: false,
80+
highlightSentences: false,
81+
highlightParagraphs: false,
82+
showTranslations: false,
83+
showQuestions: true,
84+
togglePassageVisibility: vi.fn(),
85+
setFontScale: vi.fn(),
86+
toggleHighlightWords: vi.fn(),
87+
toggleHighlightSentences: vi.fn(),
88+
toggleHighlightParagraphs: vi.fn(),
89+
toggleShowTranslations: vi.fn(),
90+
toggleShowQuestions: vi.fn(),
91+
},
92+
};
93+
94+
const createTestStore = () =>
95+
create<TextGeneratorState>()(
96+
immer((set, get, store) => {
97+
const quizSliceInstance = createQuizSlice(set, get, store);
98+
99+
const combinedState = {
100+
...mockBaseSlice,
101+
...mockUISlice,
102+
...mockSettingsSlice,
103+
...mockOtherStateAndFunctions,
104+
...quizSliceInstance,
105+
stopPassageSpeech: mockOtherStateAndFunctions.stopPassageSpeech,
106+
generateText: quizSliceInstance.generateText,
107+
resetQuizWithNewData: quizSliceInstance.resetQuizWithNewData,
108+
resetQuizState: quizSliceInstance.resetQuizState,
109+
fetchUserProgress: mockOtherStateAndFunctions.fetchUserProgress,
110+
updateAvailableVoices: mockOtherStateAndFunctions.updateAvailableVoices,
111+
};
112+
return combinedState as unknown as TextGeneratorState;
113+
})
114+
);
115+
116+
describe('quizSlice', () => {
117+
let store: ReturnType<typeof createTestStore>;
118+
119+
beforeEach(() => {
120+
store = createTestStore();
121+
vi.clearAllMocks();
122+
mockOtherStateAndFunctions.stopPassageSpeech.mockClear();
123+
vi.mocked(generateExerciseResponse).mockClear();
124+
vi.mocked(submitAnswer).mockClear();
125+
vi.mocked(submitQuestionFeedback).mockClear();
126+
});
127+
128+
it('should initialize with default values', () => {
129+
const state = store.getState();
130+
expect(state.quizData).toBeNull();
131+
expect(state.currentQuizId).toBeNull();
132+
expect(state.selectedAnswer).toBeNull();
133+
expect(state.isAnswered).toBe(false);
134+
expect(state.relevantTextRange).toBeNull();
135+
expect(state.feedbackIsCorrect).toBeNull();
136+
expect(state.feedbackCorrectAnswer).toBeNull();
137+
expect(state.feedbackCorrectExplanation).toBeNull();
138+
expect(state.feedbackChosenIncorrectExplanation).toBeNull();
139+
expect(state.feedbackRelevantText).toBeNull();
140+
expect(state.nextQuizAvailable).toBeNull();
141+
expect(state.feedbackSubmitted).toBe(false);
142+
expect(state.hoverProgressionPhase).toBe('credits');
143+
expect(state.correctAnswersInPhase).toBe(0);
144+
expect(state.hoverCreditsAvailable).toBe(INITIAL_HOVER_CREDITS);
145+
expect(state.hoverCreditsUsed).toBe(0);
146+
});
147+
148+
it('setQuizData should update quizData', () => {
149+
const mockQuizData: PartialQuizData = {
150+
paragraph: 'Test paragraph.',
151+
question: 'What is this?',
152+
options: { A: 'Option A', B: 'Option B', C: 'Option C', D: 'Option D' },
153+
language: 'en',
154+
};
155+
store.getState().setQuizData(mockQuizData);
156+
expect(store.getState().quizData).toEqual(mockQuizData);
157+
});
158+
159+
it('setSelectedAnswer should update selectedAnswer', () => {
160+
store.getState().setSelectedAnswer('A');
161+
expect(store.getState().selectedAnswer).toBe('A');
162+
});
163+
164+
it('setIsAnswered should update isAnswered', () => {
165+
store.getState().setIsAnswered(true);
166+
expect(store.getState().isAnswered).toBe(true);
167+
});
168+
169+
it('setRelevantTextRange should update relevantTextRange', () => {
170+
const range = { start: 5, end: 10 };
171+
store.getState().setRelevantTextRange(range);
172+
expect(store.getState().relevantTextRange).toEqual(range);
173+
});
174+
175+
it('setNextQuizAvailable should update nextQuizAvailable', () => {
176+
const nextInfo = {
177+
quizData: {
178+
paragraph: 'Next P',
179+
question: 'Next Q',
180+
options: { A: 'NA', B: 'NB', C: 'NC', D: 'ND' },
181+
},
182+
quizId: 2,
183+
};
184+
store.getState().setNextQuizAvailable(nextInfo);
185+
expect(store.getState().nextQuizAvailable).toEqual(nextInfo);
186+
});
187+
188+
it('resetQuizState should reset quiz related state', () => {
189+
const mockQuizData: PartialQuizData = {
190+
paragraph: 'Test paragraph.',
191+
question: 'What is this?',
192+
options: { A: 'A', B: 'B', C: 'C', D: 'D' },
193+
language: 'en',
194+
};
195+
store.setState({
196+
quizData: mockQuizData,
197+
currentQuizId: 1,
198+
selectedAnswer: 'A',
199+
isAnswered: true,
200+
relevantTextRange: { start: 0, end: 4 },
201+
feedbackIsCorrect: true,
202+
feedbackCorrectAnswer: 'A',
203+
feedbackCorrectExplanation: 'Because.',
204+
feedbackChosenIncorrectExplanation: null,
205+
feedbackRelevantText: 'Test',
206+
showQuestionSection: true,
207+
showExplanation: true,
208+
nextQuizAvailable: {
209+
quizData: { ...mockQuizData, paragraph: 'Next para' },
210+
quizId: 2,
211+
},
212+
feedbackSubmitted: true,
213+
hoverCreditsUsed: 2,
214+
});
215+
216+
store.getState().resetQuizState();
217+
218+
const state = store.getState();
219+
expect(state.quizData).toBeNull();
220+
expect(state.currentQuizId).toBeNull();
221+
expect(state.selectedAnswer).toBeNull();
222+
expect(state.isAnswered).toBe(false);
223+
expect(state.relevantTextRange).toBeNull();
224+
expect(state.feedbackIsCorrect).toBeNull();
225+
expect(state.feedbackCorrectAnswer).toBeNull();
226+
expect(state.feedbackCorrectExplanation).toBeNull();
227+
expect(state.feedbackChosenIncorrectExplanation).toBeNull();
228+
expect(state.feedbackRelevantText).toBeNull();
229+
expect(state.showQuestionSection).toBe(false);
230+
expect(state.showExplanation).toBe(false);
231+
expect(state.nextQuizAvailable).toBeNull();
232+
expect(state.feedbackSubmitted).toBe(false);
233+
expect(state.hoverCreditsUsed).toBe(0);
234+
expect(state.hoverCreditsAvailable).toBe(INITIAL_HOVER_CREDITS);
235+
});
236+
237+
it('resetQuizWithNewData should reset state and set new quiz data', () => {
238+
const newQuizData: PartialQuizData = {
239+
paragraph: 'New P',
240+
question: 'New Q',
241+
options: { A: 'NA', B: 'NB', C: 'NC', D: 'ND' },
242+
};
243+
const newQuizId = 10;
244+
const generateTextSpy = vi.spyOn(store.getState(), 'generateText');
245+
246+
store.setState({ passageLanguage: 'fr' });
247+
248+
store.getState().resetQuizWithNewData(newQuizData, newQuizId);
249+
250+
expect(mockOtherStateAndFunctions.stopPassageSpeech).toHaveBeenCalled();
251+
expect(store.getState().selectedAnswer).toBeNull();
252+
253+
const state = store.getState();
254+
expect(state.quizData).toEqual(newQuizData);
255+
expect(state.currentQuizId).toBe(newQuizId);
256+
expect(state.showQuestionSection).toBe(true);
257+
expect(state.showExplanation).toBe(false);
258+
expect(state.showContent).toBe(true);
259+
expect(state.loading).toBe(false);
260+
expect(state.error).toBeNull();
261+
expect(state.generatedPassageLanguage).toBe('fr');
262+
263+
expect(generateTextSpy).toHaveBeenCalledWith(true);
264+
});
265+
266+
it('loadNextQuiz should call resetQuizWithNewData if next quiz is available', () => {
267+
const nextQuizData: PartialQuizData = {
268+
paragraph: 'Next P',
269+
question: 'Next Q',
270+
options: { A: '1', B: '2', C: '3', D: '4' },
271+
};
272+
const nextQuizId = 11;
273+
const resetQuizWithNewDataSpy = vi.spyOn(store.getState(), 'resetQuizWithNewData');
274+
const generateTextSpy = vi.spyOn(store.getState(), 'generateText');
275+
store.setState({ nextQuizAvailable: { quizData: nextQuizData, quizId: nextQuizId } });
276+
store.getState().loadNextQuiz();
277+
expect(resetQuizWithNewDataSpy).toHaveBeenCalledWith(nextQuizData, nextQuizId);
278+
expect(generateTextSpy).toHaveBeenCalledWith(true);
279+
});
280+
281+
it('loadNextQuiz should call generateText if next quiz is not available', () => {
282+
const resetQuizWithNewDataSpy = vi.spyOn(store.getState(), 'resetQuizWithNewData');
283+
const generateTextSpy = vi.spyOn(store.getState(), 'generateText');
284+
285+
store.setState({ nextQuizAvailable: null });
286+
287+
store.getState().loadNextQuiz();
288+
289+
expect(resetQuizWithNewDataSpy).not.toHaveBeenCalled();
290+
expect(generateTextSpy).toHaveBeenCalled();
291+
});
292+
293+
it('useHoverCredit should decrement credits and return true if available', () => {
294+
store.setState({ hoverCreditsAvailable: 5, hoverCreditsUsed: 2 });
295+
const result = store.getState().useHoverCredit();
296+
expect(result).toBe(true);
297+
expect(store.getState().hoverCreditsAvailable).toBe(4);
298+
expect(store.getState().hoverCreditsUsed).toBe(3);
299+
});
300+
301+
it('useHoverCredit should return false if no credits available', () => {
302+
store.setState({ hoverCreditsAvailable: 0, hoverCreditsUsed: INITIAL_HOVER_CREDITS });
303+
const result = store.getState().useHoverCredit();
304+
expect(result).toBe(false);
305+
expect(store.getState().hoverCreditsAvailable).toBe(0);
306+
expect(store.getState().hoverCreditsUsed).toBe(INITIAL_HOVER_CREDITS);
307+
});
308+
});

0 commit comments

Comments
 (0)