495 lines
13 KiB
TypeScript
495 lines
13 KiB
TypeScript
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>): 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<React.ComponentProps<typeof QuizEditor>> = {}) => {
|
|
const defaultProps = {
|
|
quiz: createMockQuiz(),
|
|
onSave: mockOnSave,
|
|
onStartGame: mockOnStartGame,
|
|
onConfigChange: mockOnConfigChange,
|
|
onBack: mockOnBack,
|
|
showSaveButton: true,
|
|
isSaving: false,
|
|
};
|
|
|
|
return render(<QuizEditor {...defaultProps} {...props} />);
|
|
};
|
|
|
|
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(
|
|
<QuizEditor
|
|
quiz={createMockQuiz()}
|
|
onSave={mockOnSave}
|
|
onStartGame={mockOnStartGame}
|
|
onConfigChange={mockOnConfigChange}
|
|
onBack={mockOnBack}
|
|
showSaveButton={true}
|
|
defaultConfig={userDefaultConfig}
|
|
/>
|
|
);
|
|
|
|
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(
|
|
<QuizEditor
|
|
quiz={createMockQuiz({ config: quizConfig })}
|
|
onSave={mockOnSave}
|
|
onStartGame={mockOnStartGame}
|
|
onConfigChange={mockOnConfigChange}
|
|
onBack={mockOnBack}
|
|
showSaveButton={true}
|
|
defaultConfig={userDefaultConfig}
|
|
/>
|
|
);
|
|
|
|
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(
|
|
<QuizEditor
|
|
quiz={createMockQuiz()}
|
|
onSave={mockOnSave}
|
|
onStartGame={mockOnStartGame}
|
|
onConfigChange={mockOnConfigChange}
|
|
onBack={mockOnBack}
|
|
showSaveButton={true}
|
|
defaultConfig={userDefaultConfig}
|
|
/>
|
|
);
|
|
|
|
rerender(
|
|
<QuizEditor
|
|
quiz={createMockQuiz()}
|
|
onSave={mockOnSave}
|
|
onStartGame={mockOnStartGame}
|
|
onConfigChange={mockOnConfigChange}
|
|
onBack={mockOnBack}
|
|
showSaveButton={true}
|
|
defaultConfig={updatedDefaultConfig}
|
|
/>
|
|
);
|
|
|
|
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(
|
|
<QuizEditor
|
|
quiz={createMockQuiz()}
|
|
onSave={mockOnSave}
|
|
onStartGame={mockOnStartGame}
|
|
onConfigChange={mockOnConfigChange}
|
|
onBack={mockOnBack}
|
|
showSaveButton={true}
|
|
defaultConfig={DEFAULT_GAME_CONFIG}
|
|
/>
|
|
);
|
|
|
|
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(
|
|
<QuizEditor
|
|
quiz={createMockQuiz()}
|
|
onSave={mockOnSave}
|
|
onStartGame={mockOnStartGame}
|
|
onConfigChange={mockOnConfigChange}
|
|
onBack={mockOnBack}
|
|
showSaveButton={true}
|
|
defaultConfig={userDefaultConfig}
|
|
/>
|
|
);
|
|
|
|
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(
|
|
<QuizEditor
|
|
quiz={createMockQuiz()}
|
|
onSave={mockOnSave}
|
|
onStartGame={mockOnStartGame}
|
|
onConfigChange={mockOnConfigChange}
|
|
onBack={mockOnBack}
|
|
showSaveButton={true}
|
|
defaultConfig={config}
|
|
/>
|
|
);
|
|
}
|
|
|
|
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(
|
|
<QuizEditor
|
|
quiz={createMockQuiz()}
|
|
onSave={mockOnSave}
|
|
onStartGame={mockOnStartGame}
|
|
onConfigChange={mockOnConfigChange}
|
|
onBack={mockOnBack}
|
|
showSaveButton={true}
|
|
defaultConfig={userDefaultConfig}
|
|
/>
|
|
);
|
|
|
|
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(
|
|
<QuizEditor
|
|
quiz={createMockQuiz({ config: quizConfig })}
|
|
onSave={vi.fn()}
|
|
onStartGame={mockOnStartGame}
|
|
onBack={vi.fn()}
|
|
defaultConfig={defaultConfig}
|
|
/>
|
|
);
|
|
|
|
await user.click(screen.getByText(/Start Game/));
|
|
|
|
const calledConfig = mockOnStartGame.mock.calls[0][1];
|
|
expect(calledConfig.shuffleQuestions).toBe(true);
|
|
expect(calledConfig.shuffleAnswers).toBe(false);
|
|
});
|
|
});
|