Testing
MindGames uses Jest and React Testing Library for comprehensive testing. This document outlines the testing strategy, test structure, and coverage metrics.
Test Stack
| Tool | Purpose | Version |
|---|---|---|
| Jest | Test runner and assertion library | 30.x |
| React Testing Library | Component testing utilities | 14.x |
| ts-jest | TypeScript preprocessor for Jest | 29.x |
| jest-environment-jsdom | DOM simulation for Node.js | 29.x |
Running Tests
# Run all tests npm test # Run in watch mode (re-runs on file changes) npm run test:watch # Generate coverage report npm run test:coverage
Test Structure
src/__tests__/ āāā problem-generator.test.ts # Unit tests for core algorithm āāā GameContext.test.tsx # Integration tests for state āāā OperationMixSlider.test.tsx # Component tests for UI
Test Categories
1. Unit Tests: Problem Generator
Tests the core problem generation algorithm including chain generation, operation selection, and bounds validation.
describe('generateChain', () => {
it('should create problems where each result feeds into the next', () => {
const chain = generateChain(DEFAULT_CONFIG);
let currentValue = chain!.startingNumber;
for (const problem of chain!.problems) {
expect(problem.startValue).toBe(currentValue);
currentValue = problem.result;
}
});
it('should produce clean division results (no decimals)', () => {
const config = {
...DEFAULT_CONFIG,
operationMix: { add: 0, subtract: 0, multiply: 0, divide: 100 },
};
const chain = generateChain(config);
for (const problem of chain!.problems) {
if (problem.operation === 'divide') {
expect(Number.isInteger(problem.result)).toBe(true);
}
}
});
});Coverage includes:
- Chain generation with various configurations
- Worksheet generation (multiple chains)
- Operation mix selection and weighting
- Bounds validation (maxResult, allowNegativeResults)
- Utility functions (formatNumber, sumOfDigits)
2. Integration Tests: GameContext
Tests the state management flow including worksheet generation, session handling, and answer submission.
describe('Session Management', () => {
it('should calculate score on session end', () => {
const { result } = renderHook(() => useGame(), { wrapper });
act(() => {
result.current.generateNewWorksheet();
result.current.startSession();
});
const firstProblem = result.current.state.worksheet!.chains[0].problems[0];
act(() => {
result.current.submitAnswer(firstProblem.id, firstProblem.result);
});
act(() => {
result.current.endSession();
});
expect(result.current.state.session!.score.correct).toBe(1);
expect(result.current.state.session!.score.percentage).toBe(100);
});
});Coverage includes:
- Initial state verification
- Worksheet generation and storage
- Session start/end lifecycle
- Answer submission and validation
- Chain navigation (next/previous)
- Configuration changes
- Statistics calculation
3. Component Tests: OperationMixSlider
Tests the UI component behavior including preset selection, slider interactions, and constraint enforcement.
describe('Presets', () => {
it('should apply Basic preset (40/40/10/10)', () => {
render(<OperationMixSlider value={defaultMix} onChange={mockOnChange} />);
fireEvent.click(screen.getByText('Basic'));
expect(mockOnChange).toHaveBeenCalledWith({
add: 40,
subtract: 40,
multiply: 10,
divide: 10,
});
});
it('should highlight selected preset', () => {
const randomMix = { add: 25, subtract: 25, multiply: 25, divide: 25 };
render(<OperationMixSlider value={randomMix} onChange={mockOnChange} />);
const randomButton = screen.getByText('Random').closest('button');
expect(randomButton).toHaveClass('bg-primary-500');
});
});Coverage includes:
- Rendering all controls correctly
- Preset button click handling
- Increment/decrement button behavior
- Slider drag interactions
- Minimum (10%) and maximum constraints
- Visual feedback for selected presets
Test Configuration
jest.config.js
const config = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: {
'^.+\.(ts|tsx)$': ['ts-jest', {
tsconfig: {
jsx: 'react-jsx',
esModuleInterop: true,
},
}],
},
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
},
};jest.setup.ts
import '@testing-library/jest-dom';
// Mock canvas-confetti (browser API not available in Node)
jest.mock('canvas-confetti', () => jest.fn());
// Mock next/navigation
jest.mock('next/navigation', () => ({
useRouter: () => ({
push: jest.fn(),
replace: jest.fn(),
}),
}));Coverage Metrics
| Metric | Target | Current | Status |
|---|---|---|---|
| Tests Passing | 100% | 63/63 (100%) | ā Pass |
| Branches | 70% | ~75% | ā Pass |
| Functions | 70% | ~80% | ā Pass |
| Lines | 70% | ~78% | ā Pass |
| Statements | 70% | ~78% | ā Pass |
Testing Best Practices
1. Use act() for State Updates
Always wrap state-changing operations in act() to ensure React processes updates before assertions:
act(() => {
result.current.generateNewWorksheet();
});2. Isolate Hook Tests with Wrapper
Provide context providers when testing hooks that depend on them:
const wrapper = ({ children }) => (
<GameProvider>{children}</GameProvider>
);
const { result } = renderHook(() => useGame(), { wrapper });3. Test User Behavior, Not Implementation
// Good: Test what user sees
expect(screen.getByText('Basic')).toBeInTheDocument();
// Avoid: Testing internal state directly
expect(component.state.selectedPreset).toBe('basic');4. Use Descriptive Test Names
it('should apply Basic preset (40/40/10/10)', () => {});
it('should highlight selected preset', () => {});
it('should respect minimum percentage constraint', () => {});Adding New Tests
- Create test file in
src/__tests__/ - Import testing utilities and the component/module under test
- Group related tests with
describeblocks - Write individual tests with
itblocks - Run
npm testto verify
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from '@/components/MyComponent';
describe('MyComponent', () => {
it('should render correctly', () => {
render(<MyComponent />);
expect(screen.getByText('Expected Text')).toBeInTheDocument();
});
it('should handle click events', () => {
const handleClick = jest.fn();
render(<MyComponent onClick={handleClick} />);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});