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 { DefaultConfigModal } from '../../components/DefaultConfigModal'; import { DEFAULT_GAME_CONFIG } from '../../types'; import type { GameConfig } from '../../types'; describe('DefaultConfigModal', () => { const mockOnClose = vi.fn(); const mockOnChange = vi.fn(); const mockOnSave = vi.fn(); const defaultProps = { isOpen: true, onClose: mockOnClose, config: { ...DEFAULT_GAME_CONFIG }, onChange: mockOnChange, onSave: mockOnSave, saving: false, }; beforeEach(() => { vi.clearAllMocks(); }); describe('rendering', () => { it('renders nothing when isOpen is false', () => { render(); expect(screen.queryByText('Default Game Settings')).not.toBeInTheDocument(); }); it('renders modal when isOpen is true', () => { render(); expect(screen.getByText('Default Game Settings')).toBeInTheDocument(); }); it('displays subtitle explaining the settings', () => { render(); expect(screen.getByText('Applied to all new quizzes')).toBeInTheDocument(); }); it('renders GameConfigPanel with config', () => { render(); expect(screen.getByText('Shuffle Questions')).toBeInTheDocument(); expect(screen.getByText('Host Participates')).toBeInTheDocument(); }); it('renders Cancel button', () => { render(); expect(screen.getByText('Cancel')).toBeInTheDocument(); }); it('renders Save Defaults button', () => { render(); expect(screen.getByText('Save Defaults')).toBeInTheDocument(); }); it('renders close X button', () => { render(); const closeButtons = screen.getAllByRole('button'); const xButton = closeButtons.find(btn => btn.querySelector('svg.lucide-x')); expect(xButton).toBeInTheDocument(); }); }); describe('interactions - happy path', () => { it('calls onClose when Cancel is clicked', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByText('Cancel')); expect(mockOnClose).toHaveBeenCalledTimes(1); }); it('calls onClose when X button is clicked', async () => { const user = userEvent.setup(); render(); const closeButtons = screen.getAllByRole('button'); const xButton = closeButtons.find(btn => btn.querySelector('svg.lucide-x')); await user.click(xButton!); expect(mockOnClose).toHaveBeenCalledTimes(1); }); it('calls onClose when backdrop is clicked', () => { render(); const backdrop = document.querySelector('.fixed.inset-0'); fireEvent.click(backdrop!); expect(mockOnClose).toHaveBeenCalledTimes(1); }); it('does not close when modal content is clicked', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByText('Default Game Settings')); expect(mockOnClose).not.toHaveBeenCalled(); }); it('calls onSave when Save Defaults is clicked', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByText('Save Defaults')); expect(mockOnSave).toHaveBeenCalledTimes(1); }); it('calls onChange when config is modified', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByText('Shuffle Questions')); expect(mockOnChange).toHaveBeenCalledWith({ ...DEFAULT_GAME_CONFIG, shuffleQuestions: true, }); }); }); describe('interactions - unhappy path / edge cases', () => { it('disables Save button when saving is true', () => { render(); const saveButton = screen.getByText('Saving...').closest('button'); expect(saveButton).toBeDisabled(); }); it('shows Saving... text when saving', () => { render(); expect(screen.getByText('Saving...')).toBeInTheDocument(); expect(screen.queryByText('Save Defaults')).not.toBeInTheDocument(); }); it('does not call onSave when button is disabled and clicked', async () => { const user = userEvent.setup(); render(); const saveButton = screen.getByText('Saving...').closest('button')!; await user.click(saveButton); expect(mockOnSave).not.toHaveBeenCalled(); }); it('Cancel button is still clickable when saving', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByText('Cancel')); expect(mockOnClose).toHaveBeenCalledTimes(1); }); it('handles rapid clicks on Save button', async () => { const user = userEvent.setup(); render(); const saveButton = screen.getByText('Save Defaults'); await user.click(saveButton); await user.click(saveButton); await user.click(saveButton); expect(mockOnSave).toHaveBeenCalledTimes(3); }); it('remains functional after re-opening', async () => { const user = userEvent.setup(); const { rerender } = render(); await user.click(screen.getByText('Save Defaults')); expect(mockOnSave).toHaveBeenCalledTimes(1); rerender(); expect(screen.queryByText('Default Game Settings')).not.toBeInTheDocument(); rerender(); await user.click(screen.getByText('Save Defaults')); expect(mockOnSave).toHaveBeenCalledTimes(2); }); }); describe('config propagation', () => { const getCheckboxForRow = (labelText: string) => { const label = screen.getByText(labelText); const row = label.closest('[class*="bg-white rounded-xl"]')!; return row.querySelector('input[type="checkbox"]') as HTMLInputElement; }; it('displays provided config values', () => { const customConfig: GameConfig = { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true, streakBonusEnabled: true, streakThreshold: 5, }; render(); expect(getCheckboxForRow('Shuffle Questions').checked).toBe(true); expect(screen.getByDisplayValue('5')).toBeInTheDocument(); }); it('updates display when config prop changes', () => { const { rerender } = render(); expect(getCheckboxForRow('Shuffle Questions').checked).toBe(false); const newConfig = { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true }; rerender(); expect(getCheckboxForRow('Shuffle Questions').checked).toBe(true); }); }); describe('state transitions', () => { it('transitions from not saving to saving correctly', () => { const { rerender } = render(); expect(screen.getByText('Save Defaults')).toBeInTheDocument(); rerender(); expect(screen.getByText('Saving...')).toBeInTheDocument(); }); it('transitions from saving back to not saving', () => { const { rerender } = render(); expect(screen.getByText('Saving...')).toBeInTheDocument(); rerender(); expect(screen.getByText('Save Defaults')).toBeInTheDocument(); }); it('transitions from open to closed preserves state for re-open', () => { const customConfig = { ...DEFAULT_GAME_CONFIG, shuffleQuestions: true }; const { rerender } = render(); const getCheckbox = () => { const label = screen.getByText('Shuffle Questions'); const row = label.closest('[class*="bg-white rounded-xl"]')!; return row.querySelector('input[type="checkbox"]') as HTMLInputElement; }; expect(getCheckbox().checked).toBe(true); rerender(); rerender(); expect(getCheckbox().checked).toBe(true); }); }); describe('accessibility', () => { it('all interactive elements are focusable', () => { render(); const buttons = screen.getAllByRole('button'); buttons.forEach(button => { expect(button).not.toHaveAttribute('tabindex', '-1'); }); }); it('modal traps focus within when open', () => { render(); const modal = document.querySelector('[class*="bg-white rounded-2xl"]'); expect(modal).toBeInTheDocument(); }); }); describe('with all config options enabled', () => { it('renders all nested settings when all options are enabled', () => { const allEnabledConfig: GameConfig = { shuffleQuestions: true, shuffleAnswers: true, hostParticipates: true, randomNamesEnabled: false, streakBonusEnabled: true, streakThreshold: 3, streakMultiplier: 1.2, comebackBonusEnabled: true, comebackBonusPoints: 100, penaltyForWrongAnswer: true, penaltyPercent: 30, firstCorrectBonusEnabled: true, firstCorrectBonusPoints: 75, }; render(); expect(screen.getByText('Streak threshold')).toBeInTheDocument(); expect(screen.getByText('Multiplier')).toBeInTheDocument(); expect(screen.getAllByText('Bonus points').length).toBe(2); expect(screen.getByText('Penalty')).toBeInTheDocument(); }); }); describe('async save behavior', () => { it('handles async onSave that resolves', async () => { const user = userEvent.setup(); const asyncOnSave = vi.fn().mockResolvedValue(undefined); render(); await user.click(screen.getByText('Save Defaults')); expect(asyncOnSave).toHaveBeenCalledTimes(1); }); it('handles async onSave that rejects', async () => { const user = userEvent.setup(); const asyncOnSave = vi.fn().mockRejectedValue(new Error('Save failed')); render(); await user.click(screen.getByText('Save Defaults')); expect(asyncOnSave).toHaveBeenCalledTimes(1); }); }); });