Skip to main content

Testing

MindGames uses Jest and React Testing Library for comprehensive testing. This document outlines the testing strategy and coverage.

Test Stackโ€‹

ToolPurpose
JestTest runner and assertions
React Testing LibraryComponent testing
ts-jestTypeScript support
jest-environment-jsdomDOM simulation

Running Testsโ€‹

# Run all tests
npm test

# Run in watch mode
npm run test:watch

# Generate coverage report
npm run test:coverage

Test Structureโ€‹

src/__tests__/
โ”œโ”€โ”€ problem-generator.test.ts # Unit tests for core logic
โ”œโ”€โ”€ GameContext.test.tsx # Integration tests for state
โ””โ”€โ”€ OperationMixSlider.test.tsx # Component tests

Test Categoriesโ€‹

1. Unit Tests: Problem Generatorโ€‹

Tests the core problem generation algorithm.

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:

  • Chain generation
  • Worksheet generation
  • Operation mix selection
  • Bounds validation
  • Utility functions (formatNumber, sumOfDigits)

2. Integration Tests: GameContextโ€‹

Tests the state management flow.

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:

  • Initial state
  • Worksheet generation
  • Session start/end
  • Answer submission
  • Chain navigation
  • Configuration changes
  • Statistics calculation

3. Component Tests: OperationMixSliderโ€‹

Tests the UI component behavior.

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:

  • Rendering all controls
  • Preset button clicks
  • Increment/decrement buttons
  • Slider interactions
  • Minimum/maximum constraints

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
jest.mock('canvas-confetti', () => jest.fn());

// Mock next/navigation
jest.mock('next/navigation', () => ({
useRouter: () => ({
push: jest.fn(),
replace: jest.fn(),
}),
}));

Current Coverageโ€‹

MetricTargetCurrent
Tests Passing70%100% (63/63)
Branches70%~75%
Functions70%~80%
Lines70%~78%

Testing Best Practicesโ€‹

1. Use act() for State Updatesโ€‹

act(() => {
result.current.generateNewWorksheet();
});

2. Isolate Hook Tests with Wrapperโ€‹

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โ€‹

  1. Create test file in src/__tests__/
  2. Import testing utilities and component/module
  3. Group tests with describe blocks
  4. Write individual tests with it blocks
  5. Run npm test to 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();
});
});