import React from 'react'; import { render, screen, fireEvent } 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 - Game Config Integration', () => { const mockOnSave = vi.fn(); const mockOnStartGame = vi.fn(); const mockOnConfigChange = vi.fn(); const mockOnBack = vi.fn(); const defaultProps = { quiz: createMockQuiz(), onSave: mockOnSave, onStartGame: mockOnStartGame, onConfigChange: mockOnConfigChange, onBack: mockOnBack, showSaveButton: true, isSaving: false, }; beforeEach(() => { vi.clearAllMocks(); }); const findTab = (name: 'Questions' | 'Settings') => { const tabButtons = screen.getAllByRole('button'); return tabButtons.find(btn => { const text = btn.textContent || ''; if (name === 'Questions') { return text.includes('Questions') && !text.includes('Add'); } return text.includes('Settings'); })!; }; describe('config initialization', () => { it('uses DEFAULT_GAME_CONFIG when quiz has no config', () => { render(); expect(findTab('Settings')).toBeInTheDocument(); }); it('uses quiz config when provided', async () => { const user = userEvent.setup(); const quizWithConfig = createMockQuiz({ config: { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true, streakBonusEnabled: true, }, }); render(); await user.click(findTab('Settings')); const getCheckbox = (labelText: string) => { const label = screen.getByText(labelText); const row = label.closest('[class*="bg-white rounded-xl"]')!; return row.querySelector('input[type="checkbox"]') as HTMLInputElement; }; expect(getCheckbox('Shuffle Questions').checked).toBe(true); }); it('uses defaultConfig prop when quiz has no config', async () => { const user = userEvent.setup(); const customDefault: GameConfig = { ...DEFAULT_GAME_CONFIG, hostParticipates: false, penaltyForWrongAnswer: true, }; render(); await user.click(findTab('Settings')); const getCheckbox = (labelText: string) => { const label = screen.getByText(labelText); const row = label.closest('[class*="bg-white rounded-xl"]')!; return row.querySelector('input[type="checkbox"]') as HTMLInputElement; }; expect(getCheckbox('Host Participates').checked).toBe(false); expect(getCheckbox('Wrong Answer Penalty').checked).toBe(true); }); it('prioritizes quiz config over defaultConfig prop', async () => { const user = userEvent.setup(); const quizWithConfig = createMockQuiz({ config: { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true, }, }); const customDefault: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleQuestions: false, shuffleAnswers: true, }; render(); await user.click(findTab('Settings')); const getCheckbox = (labelText: string) => { const label = screen.getByText(labelText); const row = label.closest('[class*="bg-white rounded-xl"]')!; return row.querySelector('input[type="checkbox"]') as HTMLInputElement; }; expect(getCheckbox('Shuffle Questions').checked).toBe(true); expect(getCheckbox('Shuffle Answers').checked).toBe(false); }); }); describe('config panel interactions', () => { it('shows Questions tab by default', () => { render(); expect(findTab('Questions')).toBeInTheDocument(); expect(findTab('Settings')).toBeInTheDocument(); expect(screen.queryByText('Shuffle Questions')).not.toBeInTheDocument(); }); it('shows config panel when Settings tab is clicked', async () => { const user = userEvent.setup(); render(); await user.click(findTab('Settings')); expect(screen.getByText('Shuffle Questions')).toBeInTheDocument(); expect(screen.getByText('Host Participates')).toBeInTheDocument(); }); it('switches back to Questions tab when clicked', async () => { const user = userEvent.setup(); render(); await user.click(findTab('Settings')); expect(screen.getByText('Shuffle Questions')).toBeInTheDocument(); await user.click(findTab('Questions')); expect(screen.queryByText('Shuffle Questions')).not.toBeInTheDocument(); }); it('calls onConfigChange when config is modified', async () => { const user = userEvent.setup(); render(); await user.click(findTab('Settings')); await user.click(screen.getByText('Shuffle Questions')); expect(mockOnConfigChange).toHaveBeenCalledWith({ ...DEFAULT_GAME_CONFIG, shuffleQuestions: true, }); }); it('calls onConfigChange multiple times for multiple changes', async () => { const user = userEvent.setup(); render(); await user.click(findTab('Settings')); await user.click(screen.getByText('Shuffle Questions')); await user.click(screen.getByText('Shuffle Answers')); expect(mockOnConfigChange).toHaveBeenCalledTimes(2); }); }); describe('start game with config', () => { it('passes config to onStartGame', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ title: 'Test Quiz', config: DEFAULT_GAME_CONFIG, }), DEFAULT_GAME_CONFIG ); }); it('passes modified config to onStartGame', async () => { const user = userEvent.setup(); render(); await user.click(findTab('Settings')); await user.click(screen.getByText('Shuffle Questions')); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ config: expect.objectContaining({ shuffleQuestions: true, }), }), expect.objectContaining({ shuffleQuestions: true, }) ); }); it('shuffles questions when shuffleQuestions is enabled', async () => { const user = userEvent.setup(); const multiQuestionQuiz = 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: 20, options: [{ text: 'C', isCorrect: true, shape: 'circle', color: 'yellow' }, { text: 'D', isCorrect: false, shape: 'square', color: 'green' }] }, { id: 'q3', text: 'Q3', timeLimit: 20, options: [{ text: 'E', isCorrect: true, shape: 'triangle', color: 'red' }, { text: 'F', isCorrect: false, shape: 'diamond', color: 'blue' }] }, ], config: { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true }, }); render(); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ questions: expect.any(Array), }), expect.objectContaining({ shuffleQuestions: true, }) ); const calledQuiz = mockOnStartGame.mock.calls[0][0]; expect(calledQuiz.questions).toHaveLength(3); }); it('shuffles answers when shuffleAnswers is enabled', async () => { const user = userEvent.setup(); const quizWithConfig = createMockQuiz({ config: { ...DEFAULT_GAME_CONFIG, shuffleAnswers: true }, }); render(); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ questions: expect.arrayContaining([ expect.objectContaining({ options: expect.any(Array), }), ]), }), expect.objectContaining({ shuffleAnswers: true, }) ); }); }); describe('config persistence', () => { it('maintains config state across quiz title edits', async () => { const user = userEvent.setup(); render(); await user.click(findTab('Settings')); await user.click(screen.getByText('Shuffle Questions')); await user.click(screen.getByText('Test Quiz')); const titleInput = screen.getByDisplayValue('Test Quiz'); await user.clear(titleInput); await user.type(titleInput, 'New Title'); fireEvent.blur(titleInput); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ config: expect.objectContaining({ shuffleQuestions: true, }), }), expect.any(Object) ); }); it('maintains config state across question additions', async () => { const user = userEvent.setup(); render(); await user.click(findTab('Settings')); await user.click(screen.getByText('Host Participates')); await user.click(findTab('Questions')); await user.click(screen.getByText('Add Question')); expect(mockOnConfigChange).toHaveBeenCalledWith( expect.objectContaining({ hostParticipates: false, }) ); }); }); describe('edge cases', () => { it('handles quiz with all config options enabled', async () => { const user = userEvent.setup(); const fullConfigQuiz = createMockQuiz({ config: { shuffleQuestions: true, shuffleAnswers: true, hostParticipates: true, randomNamesEnabled: false, streakBonusEnabled: true, streakThreshold: 5, streakMultiplier: 1.5, comebackBonusEnabled: true, comebackBonusPoints: 100, penaltyForWrongAnswer: true, penaltyPercent: 30, firstCorrectBonusEnabled: true, firstCorrectBonusPoints: 75, timerEnabled: true, maxPlayers: 10, }, }); render(); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ config: expect.objectContaining({ shuffleQuestions: true, streakBonusEnabled: true, comebackBonusEnabled: true, penaltyForWrongAnswer: true, firstCorrectBonusEnabled: true, }), }), expect.any(Object) ); }); it('handles config without onConfigChange callback', async () => { const user = userEvent.setup(); render(); await user.click(findTab('Settings')); await user.click(screen.getByText('Shuffle Questions')); await user.click(screen.getByText(/Start Game/)); expect(mockOnStartGame).toHaveBeenCalledWith( expect.objectContaining({ config: expect.objectContaining({ shuffleQuestions: true, }), }), expect.any(Object) ); }); it('handles empty questions array', () => { const emptyQuiz = createMockQuiz({ questions: [] }); render(); const startButton = screen.getByText(/Start Game/).closest('button'); expect(startButton).toBeDisabled(); }); it('handles quiz without title', () => { const noTitleQuiz = createMockQuiz({ title: '' }); render(); expect(screen.getByText('Untitled Quiz')).toBeInTheDocument(); }); }); describe('onConfigChange callback timing', () => { it('calls onConfigChange immediately when toggle changes', async () => { const user = userEvent.setup(); render(); await user.click(findTab('Settings')); await user.click(screen.getByText('Shuffle Questions')); expect(mockOnConfigChange).toHaveBeenCalledTimes(1); }); it('calls onConfigChange for nested number input changes', async () => { const user = userEvent.setup(); const quizWithStreak = createMockQuiz({ config: { ...DEFAULT_GAME_CONFIG, streakBonusEnabled: true }, }); render(); await user.click(findTab('Settings')); const thresholdInput = screen.getByDisplayValue('3'); await user.clear(thresholdInput); await user.type(thresholdInput, '5'); expect(mockOnConfigChange).toHaveBeenCalled(); }); }); });