import { describe, it, expect, beforeEach } from 'vitest'; import type { Quiz } from '../../types'; import { DEFAULT_GAME_CONFIG } from '../../types'; const DRAFT_QUIZ_KEY = 'kaboot_draft_quiz'; interface DraftQuiz { quiz: Quiz; topic?: string; sourceQuizId?: string; } const createMockQuiz = (overrides?: Partial): Quiz => ({ title: 'Test Quiz', questions: [ { id: 'q1', text: 'What is 2+2?', timeLimit: 20, options: [ { text: '3', isCorrect: false, shape: 'triangle', color: 'red' }, { text: '4', isCorrect: true, shape: 'diamond', color: 'blue' }, { text: '5', isCorrect: false, shape: 'circle', color: 'yellow' }, { text: '6', isCorrect: false, shape: 'square', color: 'green' }, ], }, ], ...overrides, }); describe('Draft Quiz Persistence', () => { let storage: Map; const getDraftQuiz = (): DraftQuiz | null => { try { const stored = storage.get(DRAFT_QUIZ_KEY); return stored ? JSON.parse(stored) : null; } catch { return null; } }; const storeDraftQuiz = (draft: DraftQuiz) => { storage.set(DRAFT_QUIZ_KEY, JSON.stringify(draft)); }; const clearDraftQuiz = () => { storage.delete(DRAFT_QUIZ_KEY); }; beforeEach(() => { storage = new Map(); }); describe('Draft Storage Operations', () => { it('should store a draft quiz', () => { const quiz = createMockQuiz(); storeDraftQuiz({ quiz, topic: 'Math' }); expect(storage.has(DRAFT_QUIZ_KEY)).toBe(true); const stored = getDraftQuiz(); expect(stored?.quiz.title).toBe('Test Quiz'); expect(stored?.topic).toBe('Math'); }); it('should store draft quiz with sourceQuizId for library quizzes', () => { const quiz = createMockQuiz(); storeDraftQuiz({ quiz, sourceQuizId: 'quiz-abc-123' }); const stored = getDraftQuiz(); expect(stored?.sourceQuizId).toBe('quiz-abc-123'); expect(stored?.topic).toBeUndefined(); }); it('should retrieve null when no draft exists', () => { const stored = getDraftQuiz(); expect(stored).toBeNull(); }); it('should clear draft quiz', () => { const quiz = createMockQuiz(); storeDraftQuiz({ quiz }); clearDraftQuiz(); expect(storage.has(DRAFT_QUIZ_KEY)).toBe(false); expect(getDraftQuiz()).toBeNull(); }); it('should overwrite existing draft', () => { const quiz1 = createMockQuiz({ title: 'First Quiz' }); const quiz2 = createMockQuiz({ title: 'Second Quiz' }); storeDraftQuiz({ quiz: quiz1, topic: 'Topic 1' }); storeDraftQuiz({ quiz: quiz2, topic: 'Topic 2' }); const stored = getDraftQuiz(); expect(stored?.quiz.title).toBe('Second Quiz'); expect(stored?.topic).toBe('Topic 2'); }); it('should preserve quiz config in draft', () => { const quizWithConfig = createMockQuiz({ config: { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true, hostParticipates: false, }, }); storeDraftQuiz({ quiz: quizWithConfig }); const stored = getDraftQuiz(); expect(stored?.quiz.config?.shuffleQuestions).toBe(true); expect(stored?.quiz.config?.hostParticipates).toBe(false); }); }); describe('URL Path Generation for Editing', () => { const getEditTargetPath = (sourceQuizId: string | null): string => { return sourceQuizId ? `/edit/${sourceQuizId}` : '/edit/draft'; }; it('should return /edit/draft when no sourceQuizId', () => { expect(getEditTargetPath(null)).toBe('/edit/draft'); }); it('should return /edit/:quizId when sourceQuizId exists', () => { expect(getEditTargetPath('quiz-123')).toBe('/edit/quiz-123'); }); it('should handle UUID-style quiz IDs', () => { expect(getEditTargetPath('a1b2c3d4-e5f6-7890-abcd-ef1234567890')) .toBe('/edit/a1b2c3d4-e5f6-7890-abcd-ef1234567890'); }); it('should handle short alphanumeric IDs', () => { expect(getEditTargetPath('abc123')).toBe('/edit/abc123'); }); }); describe('URL Initialization for Edit Routes', () => { interface InitResult { action: string; quiz?: Quiz; sourceQuizId?: string | null; topic?: string; } const simulateEditUrlInit = ( path: string, draftQuiz: DraftQuiz | null ): InitResult => { if (path === '/edit/draft') { if (draftQuiz) { return { action: 'restoreDraft', quiz: draftQuiz.quiz, sourceQuizId: null, topic: draftQuiz.topic, }; } return { action: 'navigateHome' }; } const editMatch = path.match(/^\/edit\/([a-zA-Z0-9-]+)$/); if (editMatch) { const quizId = editMatch[1]; if (draftQuiz && draftQuiz.sourceQuizId === quizId) { return { action: 'restoreLibraryQuiz', quiz: draftQuiz.quiz, sourceQuizId: quizId, }; } return { action: 'navigateHome' }; } return { action: 'noMatch' }; }; describe('/edit/draft URL', () => { it('should restore draft when draft exists in storage', () => { const quiz = createMockQuiz(); const draft: DraftQuiz = { quiz, topic: 'Science' }; const result = simulateEditUrlInit('/edit/draft', draft); expect(result.action).toBe('restoreDraft'); expect(result.quiz?.title).toBe('Test Quiz'); expect(result.sourceQuizId).toBeNull(); expect(result.topic).toBe('Science'); }); it('should navigate home when no draft exists', () => { const result = simulateEditUrlInit('/edit/draft', null); expect(result.action).toBe('navigateHome'); }); it('should restore draft without topic (manual creation)', () => { const quiz = createMockQuiz(); const draft: DraftQuiz = { quiz }; const result = simulateEditUrlInit('/edit/draft', draft); expect(result.action).toBe('restoreDraft'); expect(result.topic).toBeUndefined(); }); }); describe('/edit/:quizId URL', () => { it('should restore library quiz when quizId matches', () => { const quiz = createMockQuiz(); const draft: DraftQuiz = { quiz, sourceQuizId: 'quiz-456' }; const result = simulateEditUrlInit('/edit/quiz-456', draft); expect(result.action).toBe('restoreLibraryQuiz'); expect(result.quiz?.title).toBe('Test Quiz'); expect(result.sourceQuizId).toBe('quiz-456'); }); it('should navigate home when quizId does not match stored draft', () => { const quiz = createMockQuiz(); const draft: DraftQuiz = { quiz, sourceQuizId: 'quiz-789' }; const result = simulateEditUrlInit('/edit/quiz-456', draft); expect(result.action).toBe('navigateHome'); }); it('should navigate home when no draft exists', () => { const result = simulateEditUrlInit('/edit/quiz-456', null); expect(result.action).toBe('navigateHome'); }); it('should navigate home when draft has no sourceQuizId', () => { const quiz = createMockQuiz(); const draft: DraftQuiz = { quiz, topic: 'AI Generated' }; const result = simulateEditUrlInit('/edit/quiz-456', draft); expect(result.action).toBe('navigateHome'); }); }); describe('URL pattern matching', () => { it('should match UUID-style quiz IDs', () => { const quiz = createMockQuiz(); const draft: DraftQuiz = { quiz, sourceQuizId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' }; const result = simulateEditUrlInit('/edit/a1b2c3d4-e5f6-7890-abcd-ef1234567890', draft); expect(result.action).toBe('restoreLibraryQuiz'); }); it('should match alphanumeric IDs', () => { const quiz = createMockQuiz(); const draft: DraftQuiz = { quiz, sourceQuizId: 'abc123XYZ' }; const result = simulateEditUrlInit('/edit/abc123XYZ', draft); expect(result.action).toBe('restoreLibraryQuiz'); }); it('should not match IDs with special characters', () => { const quiz = createMockQuiz(); const draft: DraftQuiz = { quiz, sourceQuizId: 'quiz@123' }; const result = simulateEditUrlInit('/edit/quiz@123', draft); expect(result.action).toBe('noMatch'); }); it('should not match /edit without ID', () => { const quiz = createMockQuiz(); const draft: DraftQuiz = { quiz }; const result = simulateEditUrlInit('/edit', draft); expect(result.action).toBe('noMatch'); }); it('should not match /edit/ with trailing slash only', () => { const quiz = createMockQuiz(); const draft: DraftQuiz = { quiz }; const result = simulateEditUrlInit('/edit/', draft); expect(result.action).toBe('noMatch'); }); }); }); describe('Draft Lifecycle Operations', () => { describe('loadSavedQuiz (library quiz)', () => { it('should store quiz with sourceQuizId when loading from library', () => { const quiz = createMockQuiz({ title: 'Library Quiz' }); const quizId = 'lib-quiz-123'; storeDraftQuiz({ quiz, sourceQuizId: quizId }); const stored = getDraftQuiz(); expect(stored?.quiz.title).toBe('Library Quiz'); expect(stored?.sourceQuizId).toBe(quizId); expect(stored?.topic).toBeUndefined(); }); }); describe('startQuizGen (AI generated quiz)', () => { it('should store quiz with topic when AI generates quiz', () => { const quiz = createMockQuiz({ title: 'AI Quiz' }); const topic = 'Ancient History'; storeDraftQuiz({ quiz, topic }); const stored = getDraftQuiz(); expect(stored?.quiz.title).toBe('AI Quiz'); expect(stored?.topic).toBe(topic); expect(stored?.sourceQuizId).toBeUndefined(); }); it('should store quiz with file names as topic', () => { const quiz = createMockQuiz(); const topic = 'document1.pdf, document2.pdf'; storeDraftQuiz({ quiz, topic }); const stored = getDraftQuiz(); expect(stored?.topic).toBe('document1.pdf, document2.pdf'); }); }); describe('updateQuizFromEditor', () => { it('should update stored draft when quiz is edited', () => { const originalQuiz = createMockQuiz({ title: 'Original' }); storeDraftQuiz({ quiz: originalQuiz, topic: 'Topic' }); const updatedQuiz = createMockQuiz({ title: 'Updated' }); const currentDraft = getDraftQuiz(); storeDraftQuiz({ quiz: updatedQuiz, topic: currentDraft?.topic, sourceQuizId: currentDraft?.sourceQuizId, }); const stored = getDraftQuiz(); expect(stored?.quiz.title).toBe('Updated'); expect(stored?.topic).toBe('Topic'); }); it('should preserve sourceQuizId when updating library quiz', () => { const originalQuiz = createMockQuiz({ title: 'Library Original' }); storeDraftQuiz({ quiz: originalQuiz, sourceQuizId: 'lib-123' }); const updatedQuiz = createMockQuiz({ title: 'Library Updated' }); const currentDraft = getDraftQuiz(); storeDraftQuiz({ quiz: updatedQuiz, topic: currentDraft?.topic, sourceQuizId: currentDraft?.sourceQuizId, }); const stored = getDraftQuiz(); expect(stored?.quiz.title).toBe('Library Updated'); expect(stored?.sourceQuizId).toBe('lib-123'); }); }); describe('startGameFromEditor', () => { it('should clear draft when starting game', () => { const quiz = createMockQuiz(); storeDraftQuiz({ quiz, topic: 'Test' }); expect(getDraftQuiz()).not.toBeNull(); clearDraftQuiz(); expect(getDraftQuiz()).toBeNull(); }); }); describe('backFromEditor', () => { it('should clear draft when going back from editor', () => { const quiz = createMockQuiz(); storeDraftQuiz({ quiz, sourceQuizId: 'lib-456' }); expect(getDraftQuiz()).not.toBeNull(); clearDraftQuiz(); expect(getDraftQuiz()).toBeNull(); }); }); }); describe('Non-Happy Path Scenarios', () => { describe('Corrupted storage', () => { it('should return null for corrupted JSON', () => { storage.set(DRAFT_QUIZ_KEY, 'not valid json {{{'); const stored = getDraftQuiz(); expect(stored).toBeNull(); }); it('should return null for empty string', () => { storage.set(DRAFT_QUIZ_KEY, ''); const stored = getDraftQuiz(); expect(stored).toBeNull(); }); it('should handle storage with wrong structure gracefully', () => { storage.set(DRAFT_QUIZ_KEY, JSON.stringify({ wrong: 'structure' })); const stored = getDraftQuiz(); expect(stored).not.toBeNull(); expect(stored?.quiz).toBeUndefined(); }); }); describe('Edge cases', () => { it('should handle quiz with empty questions array', () => { const quiz = createMockQuiz({ questions: [] }); storeDraftQuiz({ quiz }); const stored = getDraftQuiz(); expect(stored?.quiz.questions).toHaveLength(0); }); it('should handle quiz with empty title', () => { const quiz = createMockQuiz({ title: '' }); storeDraftQuiz({ quiz }); const stored = getDraftQuiz(); expect(stored?.quiz.title).toBe(''); }); it('should handle empty topic string', () => { const quiz = createMockQuiz(); storeDraftQuiz({ quiz, topic: '' }); const stored = getDraftQuiz(); expect(stored?.topic).toBe(''); }); it('should handle very long quiz content', () => { const longText = 'A'.repeat(10000); const quiz = createMockQuiz({ title: longText, questions: Array(100).fill(null).map((_, i) => ({ id: `q${i}`, text: longText, timeLimit: 20, options: [ { text: longText, isCorrect: true, shape: 'triangle' as const, color: 'red' as const }, ], })), }); storeDraftQuiz({ quiz }); const stored = getDraftQuiz(); expect(stored?.quiz.title).toBe(longText); expect(stored?.quiz.questions).toHaveLength(100); }); }); describe('Stale draft detection', () => { it('should detect when URL quizId does not match stored sourceQuizId', () => { const quiz = createMockQuiz(); storeDraftQuiz({ quiz, sourceQuizId: 'old-quiz-id' }); const draft = getDraftQuiz(); const urlQuizId = 'new-quiz-id'; const isStale = draft?.sourceQuizId !== urlQuizId; expect(isStale).toBe(true); }); it('should detect when accessing /edit/:id but draft is for AI quiz (no sourceQuizId)', () => { const quiz = createMockQuiz(); storeDraftQuiz({ quiz, topic: 'AI Generated' }); const draft = getDraftQuiz(); const urlQuizId = 'some-quiz-id'; const canRestore = draft?.sourceQuizId === urlQuizId; expect(canRestore).toBe(false); }); it('should detect when accessing /edit/draft but draft has sourceQuizId', () => { const quiz = createMockQuiz(); storeDraftQuiz({ quiz, sourceQuizId: 'lib-quiz' }); const draft = getDraftQuiz(); const isDraftRoute = true; const hasSourceId = !!draft?.sourceQuizId; expect(hasSourceId).toBe(true); }); }); describe('Multiple browser tabs', () => { it('should overwrite draft from another tab', () => { const quiz1 = createMockQuiz({ title: 'Tab 1 Quiz' }); storeDraftQuiz({ quiz: quiz1, topic: 'Tab 1' }); const quiz2 = createMockQuiz({ title: 'Tab 2 Quiz' }); storeDraftQuiz({ quiz: quiz2, topic: 'Tab 2' }); const stored = getDraftQuiz(); expect(stored?.quiz.title).toBe('Tab 2 Quiz'); }); }); describe('Page refresh timing', () => { it('should restore draft immediately on page load', () => { const quiz = createMockQuiz({ title: 'Refreshed Quiz' }); storeDraftQuiz({ quiz, topic: 'Refresh Test' }); const stored = getDraftQuiz(); expect(stored).not.toBeNull(); expect(stored?.quiz.title).toBe('Refreshed Quiz'); }); it('should handle refresh when storage was cleared externally', () => { const quiz = createMockQuiz(); storeDraftQuiz({ quiz }); storage.clear(); const stored = getDraftQuiz(); expect(stored).toBeNull(); }); }); }); describe('Integration: Full Edit Flow', () => { it('should support full AI quiz edit flow: generate -> edit -> refresh -> continue', () => { const generatedQuiz = createMockQuiz({ title: 'AI Generated Quiz' }); storeDraftQuiz({ quiz: generatedQuiz, topic: 'Physics' }); const afterRefresh = getDraftQuiz(); expect(afterRefresh?.quiz.title).toBe('AI Generated Quiz'); expect(afterRefresh?.topic).toBe('Physics'); expect(afterRefresh?.sourceQuizId).toBeUndefined(); const editedQuiz = createMockQuiz({ title: 'AI Generated Quiz - Edited' }); storeDraftQuiz({ quiz: editedQuiz, topic: afterRefresh?.topic, sourceQuizId: afterRefresh?.sourceQuizId, }); const afterEdit = getDraftQuiz(); expect(afterEdit?.quiz.title).toBe('AI Generated Quiz - Edited'); clearDraftQuiz(); expect(getDraftQuiz()).toBeNull(); }); it('should support full library quiz edit flow: load -> edit -> refresh -> continue', () => { const libraryQuiz = createMockQuiz({ title: 'My Saved Quiz' }); const quizId = 'saved-quiz-abc'; storeDraftQuiz({ quiz: libraryQuiz, sourceQuizId: quizId }); const afterRefresh = getDraftQuiz(); expect(afterRefresh?.quiz.title).toBe('My Saved Quiz'); expect(afterRefresh?.sourceQuizId).toBe(quizId); const editedQuiz = createMockQuiz({ title: 'My Saved Quiz - Updated' }); storeDraftQuiz({ quiz: editedQuiz, topic: afterRefresh?.topic, sourceQuizId: afterRefresh?.sourceQuizId, }); const afterEdit = getDraftQuiz(); expect(afterEdit?.quiz.title).toBe('My Saved Quiz - Updated'); expect(afterEdit?.sourceQuizId).toBe(quizId); clearDraftQuiz(); expect(getDraftQuiz()).toBeNull(); }); }); }); describe('Edit Route Authentication Protection', () => { interface AuthState { isLoading: boolean; isAuthenticated: boolean; } const shouldRedirectToAuth = ( pathname: string, auth: AuthState ): { redirect: boolean; action?: 'signin' | 'wait' | 'allow' } => { const isLibraryQuizRoute = /^\/edit\/[a-zA-Z0-9-]+$/.test(pathname) && pathname !== '/edit/draft'; if (!isLibraryQuizRoute) { return { redirect: false, action: 'allow' }; } if (auth.isLoading) { return { redirect: false, action: 'wait' }; } if (!auth.isAuthenticated) { return { redirect: true, action: 'signin' }; } return { redirect: false, action: 'allow' }; }; describe('Library quiz routes (/edit/:quizId)', () => { it('should require auth for library quiz routes', () => { const result = shouldRedirectToAuth('/edit/quiz-123', { isLoading: false, isAuthenticated: false, }); expect(result.redirect).toBe(true); expect(result.action).toBe('signin'); }); it('should allow authenticated users to view library quizzes', () => { const result = shouldRedirectToAuth('/edit/quiz-123', { isLoading: false, isAuthenticated: true, }); expect(result.redirect).toBe(false); expect(result.action).toBe('allow'); }); it('should wait while auth is loading', () => { const result = shouldRedirectToAuth('/edit/quiz-123', { isLoading: true, isAuthenticated: false, }); expect(result.redirect).toBe(false); expect(result.action).toBe('wait'); }); it('should require auth for UUID-style quiz IDs', () => { const result = shouldRedirectToAuth('/edit/a1b2c3d4-e5f6-7890-abcd-ef1234567890', { isLoading: false, isAuthenticated: false, }); expect(result.redirect).toBe(true); expect(result.action).toBe('signin'); }); }); describe('Draft routes (/edit/draft)', () => { it('should NOT require auth for draft route', () => { const result = shouldRedirectToAuth('/edit/draft', { isLoading: false, isAuthenticated: false, }); expect(result.redirect).toBe(false); expect(result.action).toBe('allow'); }); it('should allow unauthenticated users to view drafts', () => { const result = shouldRedirectToAuth('/edit/draft', { isLoading: false, isAuthenticated: false, }); expect(result.redirect).toBe(false); }); }); describe('Non-edit routes', () => { it('should NOT require auth for landing page', () => { const result = shouldRedirectToAuth('/', { isLoading: false, isAuthenticated: false, }); expect(result.redirect).toBe(false); expect(result.action).toBe('allow'); }); it('should NOT require auth for create route', () => { const result = shouldRedirectToAuth('/create', { isLoading: false, isAuthenticated: false, }); expect(result.redirect).toBe(false); expect(result.action).toBe('allow'); }); it('should NOT require auth for play route', () => { const result = shouldRedirectToAuth('/play/123456', { isLoading: false, isAuthenticated: false, }); expect(result.redirect).toBe(false); expect(result.action).toBe('allow'); }); it('should NOT require auth for host route', () => { const result = shouldRedirectToAuth('/host/123456', { isLoading: false, isAuthenticated: false, }); expect(result.redirect).toBe(false); expect(result.action).toBe('allow'); }); }); describe('Auth state transitions', () => { it('should redirect after auth finishes loading (unauthenticated)', () => { const auth = { isLoading: true, isAuthenticated: false }; const beforeLoad = shouldRedirectToAuth('/edit/quiz-123', auth); expect(beforeLoad.action).toBe('wait'); auth.isLoading = false; const afterLoad = shouldRedirectToAuth('/edit/quiz-123', auth); expect(afterLoad.redirect).toBe(true); expect(afterLoad.action).toBe('signin'); }); it('should allow access after auth finishes loading (authenticated)', () => { const auth = { isLoading: true, isAuthenticated: false }; const beforeLoad = shouldRedirectToAuth('/edit/quiz-123', auth); expect(beforeLoad.action).toBe('wait'); auth.isLoading = false; auth.isAuthenticated = true; const afterLoad = shouldRedirectToAuth('/edit/quiz-123', auth); expect(afterLoad.redirect).toBe(false); expect(afterLoad.action).toBe('allow'); }); }); describe('Edge cases', () => { it('should handle /edit without trailing path', () => { const result = shouldRedirectToAuth('/edit', { isLoading: false, isAuthenticated: false, }); expect(result.redirect).toBe(false); }); it('should handle /edit/ with trailing slash', () => { const result = shouldRedirectToAuth('/edit/', { isLoading: false, isAuthenticated: false, }); expect(result.redirect).toBe(false); }); it('should handle special characters in URL (should not match)', () => { const result = shouldRedirectToAuth('/edit/quiz@123', { isLoading: false, isAuthenticated: false, }); expect(result.redirect).toBe(false); }); }); });