# TypeScript Testing - Starter Implementation Guide

## 🚀 Day 1: Initial Setup (Step-by-Step)

### Step 1: Install Dependencies

```bash
cd /Volumes/Mac\ maxi/Users/christoph/sources/alex

# Install all TypeScript testing dependencies
npm install --save-dev \
  typescript@^5.3.0 \
  ts-jest@^29.1.0 \
  @types/jest@^29.5.0 \
  @types/node@^20.0.0 \
  @jest/globals@^29.7.0 \
  jest-mock-extended@^3.0.0 \
  tsd@^0.30.0 \
  @playwright/test@^1.40.0 \
  type-coverage@^2.27.0

# Verify installation
npm list typescript ts-jest @types/jest
```

### Step 2: Create TypeScript Configuration

**File: `tsconfig.json`**

```json
{
  "compilerOptions": {
    /* Language and Environment */
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "jsx": "preserve",
    "module": "ESNext",

    /* Module Resolution */
    "moduleResolution": "node",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@pieces/*": ["src/pieces/*"],
      "@game/*": ["src/game/*"],
      "@utils/*": ["src/utils/*"],
      "@engine/*": ["src/engine/*"],
      "@controllers/*": ["src/controllers/*"],
      "@views/*": ["src/views/*"],
      "@tests/*": ["tests/*"]
    },
    "resolveJsonModule": true,
    "types": ["jest", "node"],

    /* Type Checking */
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,

    /* Emit */
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "removeComments": false,
    "noEmit": false,

    /* Interop Constraints */
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,

    /* Skip Checking */
    "skipLibCheck": true
  },
  "include": [
    "src/**/*",
    "tests/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist",
    "coverage",
    "js/**/*"
  ]
}
```

### Step 3: Create Jest TypeScript Configuration

**File: `jest.config.ts`** (replace existing jest.config.js)

```typescript
import type { Config } from 'jest';

const config: Config = {
  // Use jsdom for DOM testing
  testEnvironment: 'jsdom',

  // Use ts-jest preset
  preset: 'ts-jest',

  // File extensions
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],

  // Test file patterns
  testMatch: [
    '**/tests/**/*.test.ts',
    '**/tests/**/*.test.tsx',
    '**/tests/**/*.spec.ts',
    '**/__tests__/**/*.ts'
  ],

  // Transform files with ts-jest
  transform: {
    '^.+\\.tsx?$': ['ts-jest', {
      tsconfig: {
        esModuleInterop: true,
        allowSyntheticDefaultImports: true,
        jsx: 'react',
        module: 'ESNext',
        target: 'ES2020',
        moduleResolution: 'node',
        resolveJsonModule: true,
        isolatedModules: true,
        strict: false, // Start lenient, tighten later
      }
    }]
  },

  // Path aliases (must match tsconfig.json)
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '^@pieces/(.*)$': '<rootDir>/src/pieces/$1',
    '^@game/(.*)$': '<rootDir>/src/game/$1',
    '^@utils/(.*)$': '<rootDir>/src/utils/$1',
    '^@engine/(.*)$': '<rootDir>/src/engine/$1',
    '^@controllers/(.*)$': '<rootDir>/src/controllers/$1',
    '^@views/(.*)$': '<rootDir>/src/views/$1',
    '^@tests/(.*)$': '<rootDir>/tests/$1'
  },

  // Coverage collection
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.test.{ts,tsx}',
    '!src/**/*.spec.{ts,tsx}',
    '!src/main.ts',
    '!**/node_modules/**'
  ],

  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html', 'json-summary', 'text-summary'],

  // Coverage thresholds
  coverageThreshold: {
    global: {
      statements: 80,
      branches: 75,
      functions: 80,
      lines: 80
    }
  },

  // Test setup
  setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],

  // ts-jest specific options
  globals: {
    'ts-jest': {
      isolatedModules: true,
      diagnostics: {
        warnOnly: true, // Don't fail on type errors initially
        ignoreCodes: [151001] // Ignore some common migration issues
      }
    }
  },

  // Test environment options
  testEnvironmentOptions: {
    url: 'http://localhost'
  },

  // Verbose output
  verbose: true,

  // Test timeout
  testTimeout: 10000,

  // Clear/reset mocks
  clearMocks: true,
  resetMocks: true,
  restoreMocks: true
};

export default config;
```

### Step 4: Migrate Test Setup

**File: `tests/setup.ts`** (migrate from tests/setup.js)

```typescript
import '@testing-library/jest-dom';
import { jest } from '@jest/globals';

// ============================================================================
// Type Definitions
// ============================================================================

interface LocalStorageMock extends Storage {
  getItem: jest.MockedFunction<(key: string) => string | null>;
  setItem: jest.MockedFunction<(key: string, value: string) => void>;
  removeItem: jest.MockedFunction<(key: string) => void>;
  clear: jest.MockedFunction<() => void>;
  key: jest.MockedFunction<(index: number) => string | null>;
  readonly length: number;
}

interface ChessPosition {
  row: number;
  col: number;
}

// ============================================================================
// Global Mocks
// ============================================================================

// Create type-safe localStorage mock
const createLocalStorageMock = (): LocalStorageMock => {
  const storage: Map<string, string> = new Map();

  return {
    getItem: jest.fn<(key: string) => string | null>((key: string) => {
      return storage.get(key) ?? null;
    }),
    setItem: jest.fn<(key: string, value: string) => void>((key: string, value: string) => {
      storage.set(key, value);
    }),
    removeItem: jest.fn<(key: string) => void>((key: string) => {
      storage.delete(key);
    }),
    clear: jest.fn<() => void>(() => {
      storage.clear();
    }),
    key: jest.fn<(index: number) => string | null>((index: number) => {
      const keys = Array.from(storage.keys());
      return keys[index] ?? null;
    }),
    get length(): number {
      return storage.size;
    }
  };
};

// Install mock
global.localStorage = createLocalStorageMock() as Storage;

// Mock console to reduce noise
global.console = {
  ...console,
  error: jest.fn(),
  warn: jest.fn(),
  log: jest.fn(),
  info: jest.fn(),
  debug: jest.fn()
} as Console;

// ============================================================================
// Custom Matchers
// ============================================================================

declare global {
  namespace jest {
    interface Matchers<R> {
      toBeValidChessPosition(): R;
      toBeValidFEN(): R;
    }
  }
}

expect.extend({
  toBeValidChessPosition(received: unknown): jest.CustomMatcherResult {
    const isValidPosition = (pos: unknown): pos is ChessPosition => {
      return (
        typeof pos === 'object' &&
        pos !== null &&
        'row' in pos &&
        'col' in pos &&
        typeof (pos as any).row === 'number' &&
        typeof (pos as any).col === 'number'
      );
    };

    if (!isValidPosition(received)) {
      return {
        message: () =>
          `expected ${JSON.stringify(received)} to be a valid chess position with numeric row and col properties`,
        pass: false
      };
    }

    const { row, col } = received;
    const isValid = row >= 0 && row < 8 && col >= 0 && col < 8;

    return {
      message: () =>
        isValid
          ? `expected ${JSON.stringify(received)} not to be a valid chess position`
          : `expected ${JSON.stringify(received)} to be a valid chess position (row and col must be 0-7, got row: ${row}, col: ${col})`,
      pass: isValid
    };
  },

  toBeValidFEN(received: unknown): jest.CustomMatcherResult {
    if (typeof received !== 'string') {
      return {
        message: () => `expected value to be a string, got ${typeof received}`,
        pass: false
      };
    }

    const fenRegex = /^([rnbqkpRNBQKP1-8]+\/){7}[rnbqkpRNBQKP1-8]+ [wb] [KQkq-]+ ([a-h][1-8]|-) \d+ \d+$/;
    const isValid = fenRegex.test(received);

    return {
      message: () =>
        isValid
          ? `expected "${received}" not to be valid FEN notation`
          : `expected "${received}" to be valid FEN notation`,
      pass: isValid
    };
  }
});

// ============================================================================
// Test Lifecycle Hooks
// ============================================================================

beforeEach(() => {
  // Clear all mocks before each test
  jest.clearAllMocks();

  // Reset localStorage
  localStorage.clear();
});

afterEach(() => {
  // Restore all mocks after each test
  jest.restoreAllMocks();
});

// ============================================================================
// Global Test Configuration
// ============================================================================

// Suppress console errors in tests unless debugging
if (!process.env.DEBUG_TESTS) {
  global.console.error = jest.fn();
  global.console.warn = jest.fn();
}

// Set reasonable timeout for all tests
jest.setTimeout(10000);
```

### Step 5: Create Test Utilities Directory

```bash
mkdir -p tests/utils
mkdir -p tests/types
mkdir -p tests/integration
mkdir -p tests/e2e
```

### Step 6: Create Type Definitions

**File: `src/types/index.ts`**

```typescript
// ============================================================================
// Core Types
// ============================================================================

export type PieceType = 'pawn' | 'knight' | 'bishop' | 'rook' | 'queen' | 'king';
export type PieceColor = 'white' | 'black';

export interface Position {
  row: number;
  col: number;
}

export interface Move {
  from: Position;
  to: Position;
  piece: PieceType;
  captured: PieceType | null;
  promotion: PieceType | null;
  castling: 'kingside' | 'queenside' | null;
  enPassant: boolean;
}

// ============================================================================
// Piece Interface
// ============================================================================

export interface Piece {
  type: PieceType;
  color: PieceColor;
  position: Position;
  hasMoved: boolean;
  getValidMoves: (board: Board, gameState?: GameState) => Position[];
}

// ============================================================================
// Board Interface
// ============================================================================

export interface Board {
  grid: (Piece | null)[][];
  getPiece(row: number, col: number): Piece | null;
  setPiece(row: number, col: number, piece: Piece | null): void;
  movePiece(fromRow: number, fromCol: number, toRow: number, toCol: number): MoveResult;
  clone(): Board;
  isInBounds(row: number, col: number): boolean;
  findKing(color: PieceColor): Position;
  getAllPieces(color?: PieceColor): Piece[];
  clear(): void;
  setupInitialPosition(): void;
}

export interface MoveResult {
  success: boolean;
  captured: Piece | null;
  error?: string;
}

// ============================================================================
// Game State Interface
// ============================================================================

export interface CastlingRights {
  whiteKingside: boolean;
  whiteQueenside: boolean;
  blackKingside: boolean;
  blackQueenside: boolean;
}

export interface GameState {
  currentTurn: PieceColor;
  board: Board;
  moveHistory: Move[];
  capturedPieces: {
    white: Piece[];
    black: Piece[];
  };
  castlingRights: CastlingRights;
  enPassantTarget: Position | null;
  halfMoveClock: number;
  fullMoveNumber: number;
  inCheck: boolean;
  isCheckmate: boolean;
  isStalemate: boolean;
}

// ============================================================================
// Validator Interface
// ============================================================================

export interface MoveValidator {
  isValidMove(from: Position, to: Position, board: Board, gameState: GameState): boolean;
  wouldBeInCheck(color: PieceColor, board: Board, after: Move): boolean;
  getValidMovesForPiece(piece: Piece, board: Board, gameState: GameState): Position[];
}
```

### Step 7: Create Test Factories

**File: `tests/utils/factories.ts`**

```typescript
import { jest } from '@jest/globals';
import type { Piece, PieceType, PieceColor, Position, Board } from '@/types';

// ============================================================================
// Piece Factory
// ============================================================================

export class TestPieceFactory {
  /**
   * Create a generic piece with mock getValidMoves
   */
  static createPiece(
    type: PieceType,
    color: PieceColor,
    position: Position,
    hasMoved: boolean = false
  ): Piece {
    return {
      type,
      color,
      position: { row: position.row, col: position.col },
      hasMoved,
      getValidMoves: jest.fn(() => []) as jest.MockedFunction<(board: Board) => Position[]>
    };
  }

  static createPawn(color: PieceColor, position: Position, hasMoved: boolean = false): Piece {
    return this.createPiece('pawn', color, position, hasMoved);
  }

  static createKnight(color: PieceColor, position: Position): Piece {
    return this.createPiece('knight', color, position);
  }

  static createBishop(color: PieceColor, position: Position): Piece {
    return this.createPiece('bishop', color, position);
  }

  static createRook(color: PieceColor, position: Position, hasMoved: boolean = false): Piece {
    return this.createPiece('rook', color, position, hasMoved);
  }

  static createQueen(color: PieceColor, position: Position): Piece {
    return this.createPiece('queen', color, position);
  }

  static createKing(color: PieceColor, position: Position, hasMoved: boolean = false): Piece {
    return this.createPiece('king', color, position, hasMoved);
  }
}

// ============================================================================
// Board Factory
// ============================================================================

export class TestBoardFactory {
  /**
   * Create an empty 8x8 board
   */
  static createEmptyBoard(): Board {
    // Import actual Board class when migrated
    const { Board } = require('@game/Board');
    const board = new Board();
    board.clear();
    return board;
  }

  /**
   * Create board with starting position
   */
  static createStartingPosition(): Board {
    const { Board } = require('@game/Board');
    const board = new Board();
    board.setupInitialPosition();
    return board;
  }

  /**
   * Create custom board position
   */
  static createCustomPosition(
    pieces: Array<{ piece: Piece; position: Position }>
  ): Board {
    const board = this.createEmptyBoard();
    pieces.forEach(({ piece, position }) => {
      board.setPiece(position.row, position.col, piece);
    });
    return board;
  }
}

// ============================================================================
// Position Helpers
// ============================================================================

export class PositionHelper {
  /**
   * Create a position from algebraic notation (e.g., "e4")
   */
  static fromAlgebraic(notation: string): Position {
    const col = notation.charCodeAt(0) - 'a'.charCodeAt(0);
    const row = 8 - parseInt(notation[1], 10);
    return { row, col };
  }

  /**
   * Convert position to algebraic notation
   */
  static toAlgebraic(position: Position): string {
    const col = String.fromCharCode('a'.charCodeAt(0) + position.col);
    const row = 8 - position.row;
    return `${col}${row}`;
  }

  /**
   * Create array of positions
   */
  static createPositions(...notations: string[]): Position[] {
    return notations.map(n => this.fromAlgebraic(n));
  }
}
```

### Step 8: Update package.json Scripts

```json
{
  "scripts": {
    "dev": "npx http-server -p 8080 -o",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:types": "tsd",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui",
    "test:all": "npm run type-check && npm run test:types && npm test",
    "type-check": "tsc --noEmit",
    "type-coverage": "type-coverage --at-least 90 --strict",
    "lint": "eslint src/**/*.ts tests/**/*.ts",
    "format": "prettier --write \"**/*.{ts,tsx,css,html,md}\"",
    "build": "tsc",
    "clean": "rm -rf dist coverage"
  }
}
```

### Step 9: Verify Setup

```bash
# Test TypeScript compilation
npm run type-check

# Should show existing tests
npm test -- --listTests

# Run current JavaScript tests (should still pass)
npm test

# Check coverage
npm run test:coverage
```

## 🎯 First Migration Example: Constants

Now let's migrate the first file as a complete example!

### Step 10: Create Source Directory Structure

```bash
mkdir -p src/utils
mkdir -p src/pieces
mkdir -p src/game
mkdir -p src/engine
mkdir -p src/controllers
mkdir -p src/views
```

### Step 11: Migrate Constants.js to TypeScript

**File: `src/utils/Constants.ts`**

```typescript
// ============================================================================
// Chess Game Constants
// ============================================================================

export const BOARD_SIZE = 8 as const;

export const PIECE_TYPES = [
  'pawn',
  'knight',
  'bishop',
  'rook',
  'queen',
  'king'
] as const;

export const COLORS = ['white', 'black'] as const;

export const STARTING_POSITION = {
  white: {
    king: { row: 7, col: 4 },
    queen: { row: 7, col: 3 },
    rooks: [
      { row: 7, col: 0 },
      { row: 7, col: 7 }
    ],
    bishops: [
      { row: 7, col: 2 },
      { row: 7, col: 5 }
    ],
    knights: [
      { row: 7, col: 1 },
      { row: 7, col: 6 }
    ],
    pawns: Array.from({ length: 8 }, (_, i) => ({ row: 6, col: i }))
  },
  black: {
    king: { row: 0, col: 4 },
    queen: { row: 0, col: 3 },
    rooks: [
      { row: 0, col: 0 },
      { row: 0, col: 7 }
    ],
    bishops: [
      { row: 0, col: 2 },
      { row: 0, col: 5 }
    ],
    knights: [
      { row: 0, col: 1 },
      { row: 0, col: 6 }
    ],
    pawns: Array.from({ length: 8 }, (_, i) => ({ row: 1, col: i }))
  }
} as const;

export const PIECE_VALUES = {
  pawn: 1,
  knight: 3,
  bishop: 3,
  rook: 5,
  queen: 9,
  king: Infinity
} as const;

export type PieceValue = typeof PIECE_VALUES[keyof typeof PIECE_VALUES];
```

### Step 12: Create Test for Constants

**File: `tests/unit/utils/Constants.test.ts`**

```typescript
import { describe, test, expect } from '@jest/globals';
import {
  BOARD_SIZE,
  PIECE_TYPES,
  COLORS,
  STARTING_POSITION,
  PIECE_VALUES
} from '@utils/Constants';

describe('Constants', () => {
  describe('BOARD_SIZE', () => {
    test('should be 8', () => {
      expect(BOARD_SIZE).toBe(8);
    });

    test('should be a number', () => {
      expect(typeof BOARD_SIZE).toBe('number');
    });
  });

  describe('PIECE_TYPES', () => {
    test('should contain all 6 piece types', () => {
      expect(PIECE_TYPES).toHaveLength(6);
      expect(PIECE_TYPES).toEqual([
        'pawn',
        'knight',
        'bishop',
        'rook',
        'queen',
        'king'
      ]);
    });

    test('should be readonly', () => {
      expect(() => {
        (PIECE_TYPES as any).push('wizard');
      }).toThrow();
    });
  });

  describe('COLORS', () => {
    test('should contain white and black', () => {
      expect(COLORS).toHaveLength(2);
      expect(COLORS).toContain('white');
      expect(COLORS).toContain('black');
    });
  });

  describe('STARTING_POSITION', () => {
    test('white king should start at e1 (7,4)', () => {
      expect(STARTING_POSITION.white.king).toEqual({ row: 7, col: 4 });
    });

    test('black king should start at e8 (0,4)', () => {
      expect(STARTING_POSITION.black.king).toEqual({ row: 0, col: 4 });
    });

    test('should have 8 pawns per color', () => {
      expect(STARTING_POSITION.white.pawns).toHaveLength(8);
      expect(STARTING_POSITION.black.pawns).toHaveLength(8);
    });

    test('white pawns should be on row 6', () => {
      STARTING_POSITION.white.pawns.forEach(pawn => {
        expect(pawn.row).toBe(6);
      });
    });

    test('black pawns should be on row 1', () => {
      STARTING_POSITION.black.pawns.forEach(pawn => {
        expect(pawn.row).toBe(1);
      });
    });
  });

  describe('PIECE_VALUES', () => {
    test('should have correct relative values', () => {
      expect(PIECE_VALUES.pawn).toBe(1);
      expect(PIECE_VALUES.knight).toBe(3);
      expect(PIECE_VALUES.bishop).toBe(3);
      expect(PIECE_VALUES.rook).toBe(5);
      expect(PIECE_VALUES.queen).toBe(9);
      expect(PIECE_VALUES.king).toBe(Infinity);
    });

    test('queen should be most valuable (except king)', () => {
      const values = Object.entries(PIECE_VALUES)
        .filter(([type]) => type !== 'king')
        .map(([, value]) => value);

      expect(Math.max(...values)).toBe(PIECE_VALUES.queen);
    });
  });
});
```

### Step 13: Run the Tests

```bash
# Run just the Constants test
npm test -- Constants.test.ts

# Should see:
# PASS  tests/unit/utils/Constants.test.ts
#   Constants
#     BOARD_SIZE
#       ✓ should be 8
#       ✓ should be a number
#     PIECE_TYPES
#       ✓ should contain all 6 piece types
#       ✓ should be readonly
#     ...
#
# Test Suites: 1 passed, 1 total
# Tests:       10 passed, 10 total
```

### Step 14: Verify Type Safety

**File: `tests/types/constants-types.test.ts`**

```typescript
import { expectType, expectError } from 'tsd';
import type { PieceValue } from '@utils/Constants';
import { PIECE_TYPES, COLORS, BOARD_SIZE } from '@utils/Constants';

// Test: BOARD_SIZE is literal type
expectType<8>(BOARD_SIZE);

// Test: PIECE_TYPES is readonly
expectError(PIECE_TYPES.push('wizard'));

// Test: PieceValue type
expectType<PieceValue>(1);
expectType<PieceValue>(3);
expectType<PieceValue>(Infinity);
expectError<PieceValue>(100);

// Test: COLORS is tuple
expectType<readonly ['white', 'black']>(COLORS);
```

```bash
# Run type tests
npm run test:types
```

### Step 15: Commit the Changes

```bash
# Add files
git add src/utils/Constants.ts
git add tests/unit/utils/Constants.test.ts
git add tests/types/constants-types.test.ts
git add tsconfig.json
git add jest.config.ts
git add tests/setup.ts
git add src/types/index.ts
git add tests/utils/factories.ts
git add package.json

# Commit
git commit -m "feat: migrate Constants to TypeScript

- Add TypeScript configuration (tsconfig.json)
- Configure Jest for TypeScript (jest.config.ts)
- Migrate test setup to TypeScript
- Create type definitions (src/types/index.ts)
- Create test utilities (factories, mocks)
- Migrate Constants.js to Constants.ts
- Add comprehensive tests for Constants
- Add type-level tests
- All tests passing (10/10 new + 124/124 existing)
- Type coverage: 100% for migrated files"

# Create PR
git push origin migrate/constants-typescript
gh pr create \
  --title "feat: Migrate Constants to TypeScript" \
  --body "Part of Issue #6 TypeScript migration

**Changes:**
- Initial TypeScript setup (tsconfig.json, jest.config.ts)
- Migrated Constants to TypeScript with full type safety
- Added comprehensive tests (unit + type-level)
- Created test utilities for future migrations

**Testing:**
- ✅ All 10 new tests passing
- ✅ All 124 existing tests passing
- ✅ Type check passing
- ✅ Coverage maintained at 80%+

**Next Steps:**
- Migrate Helpers.ts
- Migrate Piece.ts base class
- Continue with piece implementations"
```

## 🎉 Success!

You've now completed the initial setup and first migration! The foundation is in place for the remaining files.

## 📋 Next Steps

1. Repeat Steps 11-15 for each remaining file in migration order
2. Use factories and test utilities consistently
3. Keep tests passing at every step
4. Monitor coverage and type coverage
5. Create small, focused PRs

**Next File:** `Helpers.ts` - Follow the same pattern!

