Testing

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


Test Stack

ToolPurposeVersion
JestTest runner and assertion library30.x
React Testing LibraryComponent testing utilities14.x
ts-jestTypeScript preprocessor for Jest29.x
jest-environment-jsdomDOM simulation for Node.js29.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

MetricTargetCurrentStatus
Tests Passing100%63/63 (100%)āœ“ Pass
Branches70%~75%āœ“ Pass
Functions70%~80%āœ“ Pass
Lines70%~78%āœ“ Pass
Statements70%~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

  1. Create test file in src/__tests__/
  2. Import testing utilities and the component/module under test
  3. Group related 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();
  });

  it('should handle click events', () => {
    const handleClick = jest.fn();
    render(<MyComponent onClick={handleClick} />);

    fireEvent.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});

Next Steps