Add more tests
This commit is contained in:
parent
62281e1124
commit
5974134ad8
7 changed files with 1418 additions and 231 deletions
|
|
@ -1,16 +1,14 @@
|
|||
import React from 'react';
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useQuizLibrary } from '../../hooks/useQuizLibrary';
|
||||
import type { Quiz } from '../../types';
|
||||
|
||||
const mockAuthFetch = vi.fn();
|
||||
const mockIsAuthenticated = vi.fn(() => true);
|
||||
|
||||
vi.mock('../../hooks/useAuthenticatedFetch', () => ({
|
||||
useAuthenticatedFetch: () => ({
|
||||
authFetch: mockAuthFetch,
|
||||
isAuthenticated: mockIsAuthenticated(),
|
||||
isAuthenticated: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
@ -39,38 +37,35 @@ const createMockQuiz = (overrides?: Partial<Quiz>): Quiz => ({
|
|||
...overrides,
|
||||
});
|
||||
|
||||
describe('useQuizLibrary - updateQuiz', () => {
|
||||
describe('useQuizLibrary', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockIsAuthenticated.mockReturnValue(true);
|
||||
mockAuthFetch.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('happy path', () => {
|
||||
it('successfully updates a quiz', async () => {
|
||||
describe('fetchQuizzes', () => {
|
||||
it('fetches and stores quizzes', async () => {
|
||||
const mockQuizzes = [
|
||||
{ id: '1', title: 'Quiz 1', source: 'manual', questionCount: 5, createdAt: '2024-01-01', updatedAt: '2024-01-01' },
|
||||
{ id: '2', title: 'Quiz 2', source: 'ai_generated', questionCount: 10, createdAt: '2024-01-02', updatedAt: '2024-01-02' },
|
||||
];
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'quiz-123' }),
|
||||
json: () => Promise.resolve(mockQuizzes),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz({ title: 'Updated Quiz' });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.updateQuiz('quiz-123', quiz);
|
||||
await result.current.fetchQuizzes();
|
||||
});
|
||||
|
||||
expect(mockAuthFetch).toHaveBeenCalledWith('/api/quizzes/quiz-123', {
|
||||
method: 'PUT',
|
||||
body: expect.stringContaining('Updated Quiz'),
|
||||
});
|
||||
expect(result.current.saving).toBe(false);
|
||||
expect(result.current.quizzes).toEqual(mockQuizzes);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
it('sets saving to true during update', async () => {
|
||||
it('sets loading to true during fetch', async () => {
|
||||
let resolvePromise: (value: unknown) => void;
|
||||
const pendingPromise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
|
|
@ -78,14 +73,97 @@ describe('useQuizLibrary - updateQuiz', () => {
|
|||
mockAuthFetch.mockReturnValueOnce(pendingPromise);
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz();
|
||||
|
||||
act(() => {
|
||||
result.current.updateQuiz('quiz-123', quiz);
|
||||
result.current.fetchQuizzes();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.saving).toBe(true);
|
||||
expect(result.current.loading).toBe(true);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
resolvePromise!({ ok: true, json: () => Promise.resolve([]) });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles 500 server error', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchQuizzes();
|
||||
});
|
||||
|
||||
expect(result.current.error).toBe('Server error. Please try again.');
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('handles generic error', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchQuizzes();
|
||||
});
|
||||
|
||||
expect(result.current.error).toBe('Failed to load your quizzes.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadQuiz', () => {
|
||||
it('loads and returns a quiz', async () => {
|
||||
const mockQuiz = {
|
||||
id: 'quiz-123',
|
||||
title: 'Loaded Quiz',
|
||||
source: 'manual',
|
||||
questions: [],
|
||||
createdAt: '2024-01-01',
|
||||
updatedAt: '2024-01-01',
|
||||
};
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockQuiz),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
let loadedQuiz;
|
||||
await act(async () => {
|
||||
loadedQuiz = await result.current.loadQuiz('quiz-123');
|
||||
});
|
||||
|
||||
expect(loadedQuiz).toEqual(mockQuiz);
|
||||
expect(mockAuthFetch).toHaveBeenCalledWith('/api/quizzes/quiz-123');
|
||||
});
|
||||
|
||||
it('sets and clears loadingQuizId', async () => {
|
||||
let resolvePromise: (value: unknown) => void;
|
||||
const pendingPromise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
mockAuthFetch.mockReturnValueOnce(pendingPromise);
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
act(() => {
|
||||
result.current.loadQuiz('quiz-123');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loadingQuizId).toBe('quiz-123');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
|
|
@ -93,105 +171,315 @@ describe('useQuizLibrary - updateQuiz', () => {
|
|||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.saving).toBe(false);
|
||||
expect(result.current.loadingQuizId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('sends correct request body structure', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz({
|
||||
title: 'My Quiz',
|
||||
questions: [
|
||||
{
|
||||
id: 'q1',
|
||||
text: 'Question 1',
|
||||
timeLimit: 30,
|
||||
options: [
|
||||
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red', reason: 'Correct!' },
|
||||
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.updateQuiz('quiz-456', quiz);
|
||||
});
|
||||
|
||||
const [, options] = mockAuthFetch.mock.calls[0];
|
||||
const body = JSON.parse(options.body);
|
||||
|
||||
expect(body.title).toBe('My Quiz');
|
||||
expect(body.questions).toHaveLength(1);
|
||||
expect(body.questions[0].text).toBe('Question 1');
|
||||
expect(body.questions[0].timeLimit).toBe(30);
|
||||
expect(body.questions[0].options[0].reason).toBe('Correct!');
|
||||
});
|
||||
|
||||
it('handles quiz with multiple questions', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz({
|
||||
questions: [
|
||||
{
|
||||
id: 'q1',
|
||||
text: 'Q1',
|
||||
timeLimit: 20,
|
||||
options: [
|
||||
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
||||
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'q2',
|
||||
text: 'Q2',
|
||||
timeLimit: 25,
|
||||
options: [
|
||||
{ text: 'C', isCorrect: false, shape: 'circle', color: 'yellow' },
|
||||
{ text: 'D', isCorrect: true, shape: 'square', color: 'green' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.updateQuiz('quiz-789', quiz);
|
||||
});
|
||||
|
||||
const [, options] = mockAuthFetch.mock.calls[0];
|
||||
const body = JSON.parse(options.body);
|
||||
expect(body.questions).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unhappy path - API errors', () => {
|
||||
it('handles 404 not found error', async () => {
|
||||
it('handles 404 not found', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.loadQuiz('non-existent');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Quiz not found. It may have been deleted.');
|
||||
}
|
||||
|
||||
expect(result.current.loadingQuizId).toBeNull();
|
||||
});
|
||||
|
||||
it('handles generic error', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.loadQuiz('quiz-123');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Failed to load quiz.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveQuiz', () => {
|
||||
it('saves a quiz and returns the ID', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'new-quiz-id' }),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz();
|
||||
|
||||
await expect(
|
||||
act(async () => {
|
||||
await result.current.updateQuiz('non-existent', quiz);
|
||||
})
|
||||
).rejects.toThrow('Quiz not found');
|
||||
let savedId;
|
||||
await act(async () => {
|
||||
savedId = await result.current.saveQuiz(quiz, 'manual');
|
||||
});
|
||||
|
||||
expect(savedId).toBe('new-quiz-id');
|
||||
expect(result.current.saving).toBe(false);
|
||||
});
|
||||
|
||||
it('sends correct request body for manual quiz', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'id' }),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz({ title: 'Manual Quiz' });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.saveQuiz(quiz, 'manual');
|
||||
});
|
||||
|
||||
const [, options] = mockAuthFetch.mock.calls[0];
|
||||
const body = JSON.parse(options.body);
|
||||
expect(body.title).toBe('Manual Quiz');
|
||||
expect(body.source).toBe('manual');
|
||||
expect(body.aiTopic).toBeUndefined();
|
||||
});
|
||||
|
||||
it('sends aiTopic for AI generated quiz', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'id' }),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.saveQuiz(quiz, 'ai_generated', 'Science');
|
||||
});
|
||||
|
||||
const [, options] = mockAuthFetch.mock.calls[0];
|
||||
const body = JSON.parse(options.body);
|
||||
expect(body.source).toBe('ai_generated');
|
||||
expect(body.aiTopic).toBe('Science');
|
||||
});
|
||||
|
||||
it('includes gameConfig when present', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'id' }),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz({
|
||||
config: {
|
||||
shuffleQuestions: true,
|
||||
shuffleAnswers: false,
|
||||
hostParticipates: true,
|
||||
randomNamesEnabled: false,
|
||||
streakBonusEnabled: false,
|
||||
streakThreshold: 3,
|
||||
streakMultiplier: 1.1,
|
||||
comebackBonusEnabled: false,
|
||||
comebackBonusPoints: 50,
|
||||
penaltyForWrongAnswer: false,
|
||||
penaltyPercent: 25,
|
||||
firstCorrectBonusEnabled: false,
|
||||
firstCorrectBonusPoints: 50,
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.saveQuiz(quiz, 'manual');
|
||||
});
|
||||
|
||||
const [, options] = mockAuthFetch.mock.calls[0];
|
||||
const body = JSON.parse(options.body);
|
||||
expect(body.gameConfig.shuffleQuestions).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects quiz without title', async () => {
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz({ title: '' });
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.saveQuiz(quiz, 'manual');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Quiz must have a title');
|
||||
}
|
||||
|
||||
expect(mockAuthFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects quiz with whitespace-only title', async () => {
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz({ title: ' ' });
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.saveQuiz(quiz, 'manual');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Quiz must have a title');
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects quiz without questions', async () => {
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz({ questions: [] });
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.saveQuiz(quiz, 'manual');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Quiz must have at least one question');
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects quiz with question without text', async () => {
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz({
|
||||
questions: [{
|
||||
id: 'q1',
|
||||
text: '',
|
||||
timeLimit: 20,
|
||||
options: [
|
||||
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
||||
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
||||
],
|
||||
}],
|
||||
});
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.saveQuiz(quiz, 'manual');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('All questions must have text');
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects question with less than 2 options', async () => {
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz({
|
||||
questions: [{
|
||||
id: 'q1',
|
||||
text: 'Question?',
|
||||
timeLimit: 20,
|
||||
options: [
|
||||
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
||||
],
|
||||
}],
|
||||
});
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.saveQuiz(quiz, 'manual');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Each question must have at least 2 options');
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects question without correct answer', async () => {
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz({
|
||||
questions: [{
|
||||
id: 'q1',
|
||||
text: 'Question?',
|
||||
timeLimit: 20,
|
||||
options: [
|
||||
{ text: 'A', isCorrect: false, shape: 'triangle', color: 'red' },
|
||||
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
||||
],
|
||||
}],
|
||||
});
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.saveQuiz(quiz, 'manual');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Each question must have a correct answer');
|
||||
}
|
||||
});
|
||||
|
||||
it('prevents double save when already saving', async () => {
|
||||
let resolveFirstSave: (value: unknown) => void;
|
||||
const firstSavePromise = new Promise((resolve) => {
|
||||
resolveFirstSave = resolve;
|
||||
});
|
||||
mockAuthFetch.mockReturnValueOnce(firstSavePromise);
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz();
|
||||
|
||||
let firstSaveResult: Promise<string>;
|
||||
act(() => {
|
||||
firstSaveResult = result.current.saveQuiz(quiz, 'manual');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.saving).toBe(true);
|
||||
});
|
||||
|
||||
let secondSaveError: Error | null = null;
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.saveQuiz(quiz, 'manual');
|
||||
} catch (e) {
|
||||
secondSaveError = e as Error;
|
||||
}
|
||||
});
|
||||
|
||||
expect(secondSaveError).not.toBeNull();
|
||||
expect(secondSaveError!.message).toBe('Save already in progress');
|
||||
expect(mockAuthFetch).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
resolveFirstSave!({ ok: true, json: () => Promise.resolve({ id: 'test-id' }) });
|
||||
await firstSaveResult!;
|
||||
});
|
||||
|
||||
expect(result.current.saving).toBe(false);
|
||||
});
|
||||
|
||||
it('handles 400 validation error', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz();
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.saveQuiz(quiz, 'manual');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Invalid quiz data. Please check and try again.');
|
||||
}
|
||||
});
|
||||
|
||||
it('handles generic server error', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
|
|
@ -201,49 +489,81 @@ describe('useQuizLibrary - updateQuiz', () => {
|
|||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz();
|
||||
|
||||
await expect(
|
||||
act(async () => {
|
||||
await result.current.updateQuiz('quiz-123', quiz);
|
||||
})
|
||||
).rejects.toThrow('Failed to update quiz');
|
||||
});
|
||||
|
||||
it('handles network error', async () => {
|
||||
mockAuthFetch.mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz();
|
||||
|
||||
await expect(
|
||||
act(async () => {
|
||||
await result.current.updateQuiz('quiz-123', quiz);
|
||||
})
|
||||
).rejects.toThrow('Network error');
|
||||
|
||||
expect(result.current.saving).toBe(false);
|
||||
});
|
||||
|
||||
it('handles timeout/abort error', async () => {
|
||||
const abortError = new Error('The operation was aborted');
|
||||
abortError.name = 'AbortError';
|
||||
mockAuthFetch.mockRejectedValueOnce(abortError);
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz();
|
||||
|
||||
await expect(
|
||||
act(async () => {
|
||||
await result.current.updateQuiz('quiz-123', quiz);
|
||||
})
|
||||
).rejects.toThrow();
|
||||
|
||||
expect(result.current.saving).toBe(false);
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.saveQuiz(quiz, 'manual');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Failed to save quiz.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('unhappy path - edge cases', () => {
|
||||
it('resets saving state even on error', async () => {
|
||||
mockAuthFetch.mockRejectedValueOnce(new Error('Server error'));
|
||||
describe('deleteQuiz', () => {
|
||||
it('handles 404 not found', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.deleteQuiz('non-existent');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Quiz not found.');
|
||||
}
|
||||
});
|
||||
|
||||
it('handles generic server error', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.deleteQuiz('quiz-123');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Failed to delete quiz.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateQuizConfig', () => {
|
||||
it('throws error on failure', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.updateQuizConfig('quiz-123', {} as any);
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Failed to update config');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateQuiz', () => {
|
||||
it('handles generic server error', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz();
|
||||
|
|
@ -252,11 +572,10 @@ describe('useQuizLibrary - updateQuiz', () => {
|
|||
await act(async () => {
|
||||
await result.current.updateQuiz('quiz-123', quiz);
|
||||
});
|
||||
} catch {
|
||||
// Expected to throw
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Failed to update quiz.');
|
||||
}
|
||||
|
||||
expect(result.current.saving).toBe(false);
|
||||
});
|
||||
|
||||
it('handles empty quiz ID', async () => {
|
||||
|
|
@ -268,86 +587,14 @@ describe('useQuizLibrary - updateQuiz', () => {
|
|||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz();
|
||||
|
||||
await expect(
|
||||
act(async () => {
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.updateQuiz('', quiz);
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('handles quiz with empty title', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz({ title: '' });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.updateQuiz('quiz-123', quiz);
|
||||
});
|
||||
|
||||
const [, options] = mockAuthFetch.mock.calls[0];
|
||||
const body = JSON.parse(options.body);
|
||||
expect(body.title).toBe('');
|
||||
});
|
||||
|
||||
it('strips undefined reason fields to null', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz({
|
||||
questions: [
|
||||
{
|
||||
id: 'q1',
|
||||
text: 'Q',
|
||||
timeLimit: 20,
|
||||
options: [
|
||||
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red', reason: undefined },
|
||||
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.updateQuiz('quiz-123', quiz);
|
||||
});
|
||||
|
||||
const [, options] = mockAuthFetch.mock.calls[0];
|
||||
const body = JSON.parse(options.body);
|
||||
expect(body.questions[0].options[0]).not.toHaveProperty('reason');
|
||||
});
|
||||
});
|
||||
|
||||
describe('concurrent operations', () => {
|
||||
it('allows update after save completes', async () => {
|
||||
mockAuthFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'new-quiz' }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
const quiz = createMockQuiz();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.saveQuiz(quiz, 'manual');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.updateQuiz('new-quiz', { ...quiz, title: 'Updated' });
|
||||
});
|
||||
|
||||
expect(mockAuthFetch).toHaveBeenCalledTimes(2);
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Quiz not found.');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue