# 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: { '^@/(.*)$': '/src/$1', '^@pieces/(.*)$': '/src/pieces/$1', '^@game/(.*)$': '/src/game/$1', '^@utils/(.*)$': '/src/utils/$1', '^@engine/(.*)$': '/src/engine/$1', '^@controllers/(.*)$': '/src/controllers/$1', '^@views/(.*)$': '/src/views/$1', '^@tests/(.*)$': '/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: ['/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 = 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 { 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(1); expectType(3); expectType(Infinity); expectError(100); // Test: COLORS is tuple expectType(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!