import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { QuizEditor } from '../../components/QuizEditor'; import { DEFAULT_GAME_CONFIG } from '../../types'; import type { Quiz, GameConfig } from '../../types'; vi.mock('uuid', () => ({ v4: () => 'mock-uuid-' + Math.random().toString(36).substr(2, 9), })); 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('QuizEditor - Async Default Config Loading', () => { const mockOnSave = vi.fn(); const mockOnStartGame = vi.fn(); const mockOnConfigChange = vi.fn(); const mockOnBack = vi.fn(); beforeEach(() => { vi.clearAllMocks(); }); const renderEditor = (props: Partial> = {}) => { const defaultProps = { quiz: createMockQuiz(), onSave: mockOnSave, onStartGame: mockOnStartGame, onConfigChange: mockOnConfigChange, onBack: mockOnBack, showSaveButton: true, isSaving: false, }; return render(); }; describe('Initial config state', () => { it('should use DEFAULT_GAME_CONFIG when quiz has no config and no defaultConfig prop', async () => { const user = userEvent.setup(); renderEditor(); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ config: DEFAULT_GAME_CONFIG, }), DEFAULT_GAME_CONFIG ); }); it('should use quiz.config when provided, ignoring defaultConfig prop', async () => { const user = userEvent.setup(); const quizConfig: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true, hostParticipates: false, }; renderEditor({ quiz: createMockQuiz({ config: quizConfig }), defaultConfig: { ...DEFAULT_GAME_CONFIG, shuffleAnswers: true, }, }); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ config: expect.objectContaining({ shuffleQuestions: true, hostParticipates: false, shuffleAnswers: false, }), }), expect.any(Object) ); }); }); describe('Async defaultConfig loading (race condition fix)', () => { it('should apply defaultConfig when it loads after initial render', async () => { const user = userEvent.setup(); const userDefaultConfig: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true, streakBonusEnabled: true, }; const { rerender } = renderEditor({ defaultConfig: DEFAULT_GAME_CONFIG, }); rerender( ); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ config: expect.objectContaining({ shuffleQuestions: true, streakBonusEnabled: true, }), }), expect.any(Object) ); }); it('should NOT override quiz.config when defaultConfig loads later', async () => { const user = userEvent.setup(); const quizConfig: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleQuestions: false, hostParticipates: false, }; const userDefaultConfig: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true, hostParticipates: true, }; const { rerender } = renderEditor({ quiz: createMockQuiz({ config: quizConfig }), defaultConfig: DEFAULT_GAME_CONFIG, }); rerender( ); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ config: expect.objectContaining({ shuffleQuestions: false, hostParticipates: false, }), }), expect.any(Object) ); }); it('should only apply defaultConfig once (not on every change)', async () => { const user = userEvent.setup(); const userDefaultConfig: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true, }; const updatedDefaultConfig: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true, shuffleAnswers: true, }; const { rerender } = renderEditor({ defaultConfig: DEFAULT_GAME_CONFIG, }); rerender( ); rerender( ); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ config: expect.objectContaining({ shuffleQuestions: true, shuffleAnswers: false, }), }), expect.any(Object) ); }); it('should NOT apply defaultConfig if it equals DEFAULT_GAME_CONFIG', async () => { const user = userEvent.setup(); const { rerender } = renderEditor({ defaultConfig: undefined, }); rerender( ); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ config: DEFAULT_GAME_CONFIG, }), DEFAULT_GAME_CONFIG ); }); }); describe('User config modifications after async load', () => { it('should preserve user modifications after defaultConfig loads', async () => { const user = userEvent.setup(); const userDefaultConfig: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true, }; const { rerender } = renderEditor({ defaultConfig: DEFAULT_GAME_CONFIG, }); await user.click(screen.getByRole('button', { name: /Settings/i })); await user.click(screen.getByText('Host Participates')); rerender( ); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ config: expect.objectContaining({ hostParticipates: false, }), }), expect.any(Object) ); }); }); describe('Page refresh simulation', () => { it('should correctly initialize config after page refresh with quiz.config', async () => { const user = userEvent.setup(); const savedConfig: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true, streakBonusEnabled: true, hostParticipates: false, }; renderEditor({ quiz: createMockQuiz({ config: savedConfig }), defaultConfig: DEFAULT_GAME_CONFIG, }); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ config: expect.objectContaining({ shuffleQuestions: true, streakBonusEnabled: true, hostParticipates: false, }), }), expect.any(Object) ); }); it('should apply user defaults on refresh when quiz has no config', async () => { const user = userEvent.setup(); const userDefaultConfig: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleAnswers: true, penaltyForWrongAnswer: true, }; renderEditor({ quiz: createMockQuiz(), defaultConfig: userDefaultConfig, }); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ config: expect.objectContaining({ shuffleAnswers: true, penaltyForWrongAnswer: true, }), }), expect.any(Object) ); }); }); describe('Non-happy path scenarios', () => { it('should handle undefined defaultConfig prop', async () => { const user = userEvent.setup(); renderEditor({ defaultConfig: undefined, }); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ config: DEFAULT_GAME_CONFIG, }), DEFAULT_GAME_CONFIG ); }); it('should handle rapid defaultConfig changes', async () => { const user = userEvent.setup(); const configs = [ { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true }, { ...DEFAULT_GAME_CONFIG, shuffleAnswers: true }, { ...DEFAULT_GAME_CONFIG, hostParticipates: false }, ]; const { rerender } = renderEditor({ defaultConfig: DEFAULT_GAME_CONFIG, }); for (const config of configs) { rerender( ); } await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledTimes(1); }); it('should apply defaultConfig even when component rerenders', async () => { const user = userEvent.setup(); const userDefaultConfig: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true, hostParticipates: false, }; const { rerender } = renderEditor({ defaultConfig: DEFAULT_GAME_CONFIG, }); rerender( ); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ config: expect.objectContaining({ shuffleQuestions: true, hostParticipates: false, }), }), expect.any(Object) ); }); }); }); describe('QuizEditor - Config Priority Order', () => { const mockOnStartGame = vi.fn(); beforeEach(() => { vi.clearAllMocks(); }); it('should follow priority: quiz.config > defaultConfig > DEFAULT_GAME_CONFIG', async () => { const user = userEvent.setup(); const quizConfig: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true, }; const defaultConfig: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleAnswers: true, }; render( ); await user.click(screen.getByText(/Start Game/)); const calledConfig = mockOnStartGame.mock.calls[0][1]; expect(calledConfig.shuffleQuestions).toBe(true); expect(calledConfig.shuffleAnswers).toBe(false); }); });