# TypeScript Migration Testing Strategy ## Executive Summary This document outlines the comprehensive testing strategy for migrating the chess game from JavaScript to TypeScript while maintaining 100% test coverage and ensuring zero regressions. **Current State:** - 7 test files (unit tests for pieces and game logic) - 124 passing tests across all components - Jest with babel-jest transformer - Custom test matchers for chess-specific validations - jsdom environment for DOM testing **Migration Goal:** - Maintain all 124 tests in passing state - Add type safety to test code - Implement type-level testing - Achieve 90%+ type coverage - Zero regression tolerance --- ## Phase 1: Jest + TypeScript Configuration ### 1.1 Dependencies Installation ```bash npm install --save-dev \ typescript \ ts-jest \ @types/jest \ @types/node \ @jest/globals \ jest-mock-extended ``` **Package Purposes:** - `typescript`: TypeScript compiler - `ts-jest`: Jest transformer for TypeScript - `@types/jest`: TypeScript definitions for Jest - `@types/node`: Node.js type definitions - `@jest/globals`: Type-safe Jest globals - `jest-mock-extended`: Advanced mocking with type safety ### 1.2 Jest Configuration Migration **File:** `jest.config.ts` (new) ```typescript import type { Config } from 'jest'; const config: Config = { // Test environment testEnvironment: 'jsdom', // TypeScript transformation preset: 'ts-jest', // Module resolution moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], // Test patterns testMatch: [ '**/tests/**/*.test.ts', '**/tests/**/*.test.tsx', '**/__tests__/**/*.ts', '**/__tests__/**/*.tsx' ], // Transform configuration transform: { '^.+\\.tsx?$': ['ts-jest', { tsconfig: { esModuleInterop: true, allowSyntheticDefaultImports: true, jsx: 'react', module: 'ESNext', target: 'ES2020', moduleResolution: 'node', resolveJsonModule: true, isolatedModules: true } }], '^.+\\.jsx?$': 'babel-jest' // Keep for transition period }, // Module name mapper for path aliases 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' }, // Coverage configuration collectCoverageFrom: [ 'src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!src/**/*.test.{ts,tsx}', '!src/main.ts', '!**/node_modules/**' ], coverageDirectory: 'coverage', coverageReporters: ['text', 'lcov', 'html', 'json-summary'], // Restore coverage thresholds with TypeScript coverageThreshold: { global: { statements: 80, branches: 75, functions: 80, lines: 80 } }, // Test setup setupFilesAfterEnv: ['/tests/setup.ts'], // Globals configuration globals: { 'ts-jest': { isolatedModules: true, diagnostics: { warnOnly: true // Don't fail on type errors during transition } } }, // Verbose output verbose: true, // Test timeout testTimeout: 10000, // Clear mocks between tests clearMocks: true, restoreMocks: true, resetMocks: true }; export default config; ``` ### 1.3 Test Setup Migration **File:** `tests/setup.ts` (migrated from setup.js) ```typescript import '@testing-library/jest-dom'; import { jest } from '@jest/globals'; // Type-safe localStorage mock interface LocalStorageMock { 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>; length: number; key: jest.MockedFunction<(index: number) => string | null>; } const createLocalStorageMock = (): LocalStorageMock => ({ getItem: jest.fn<(key: string) => string | null>(), setItem: jest.fn<(key: string, value: string) => void>(), removeItem: jest.fn<(key: string) => void>(), clear: jest.fn<() => void>(), length: 0, key: jest.fn<(index: number) => string | null>() }); global.localStorage = createLocalStorageMock() as Storage; // Type-safe console mock global.console = { ...console, error: jest.fn(), warn: jest.fn(), log: jest.fn(), info: jest.fn(), debug: jest.fn() }; // Custom matchers with TypeScript types interface ChessPosition { row: number; col: number; } declare global { namespace jest { interface Matchers { toBeValidChessPosition(): R; toBeValidFEN(): R; } } } expect.extend({ toBeValidChessPosition(received: unknown) { const pos = received as ChessPosition; const isValid = typeof pos === 'object' && pos !== null && 'row' in pos && 'col' in pos && pos.row >= 0 && pos.row < 8 && pos.col >= 0 && pos.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)`, pass: isValid }; }, toBeValidFEN(received: unknown) { const fenRegex = /^([rnbqkpRNBQKP1-8]+\/){7}[rnbqkpRNBQKP1-8]+ [wb] [KQkq-]+ ([a-h][1-8]|-) \d+ \d+$/; const isValid = typeof received === 'string' && fenRegex.test(received); return { message: () => isValid ? `expected "${received}" not to be valid FEN` : `expected "${received}" to be valid FEN notation`, pass: isValid }; } }); // Reset mocks before each test beforeEach(() => { jest.clearAllMocks(); }); // Clean up after each test afterEach(() => { jest.restoreAllMocks(); }); ``` --- ## Phase 2: Test File Migration Strategy ### 2.1 Migration Approach **Incremental Migration Pattern:** 1. Migrate source file to TypeScript 2. Immediately migrate corresponding test file 3. Ensure all tests pass before moving to next file 4. Run full test suite after each migration 5. Commit after each successful file pair migration ### 2.2 Test File Migration Order **Priority 1: Core Types & Utilities (No Dependencies)** 1. `src/utils/Constants.ts` → `tests/unit/utils/Constants.test.ts` 2. `src/utils/Helpers.ts` → `tests/unit/utils/Helpers.test.ts` **Priority 2: Base Classes** 3. `src/pieces/Piece.ts` → `tests/unit/pieces/Piece.test.ts` 4. `src/game/Board.ts` → `tests/unit/game/Board.test.ts` (already exists) **Priority 3: Piece Implementations** 5. `src/pieces/Pawn.ts` → `tests/unit/pieces/Pawn.test.ts` 6. `src/pieces/Knight.ts` → `tests/unit/pieces/Knight.test.ts` 7. `src/pieces/Bishop.ts` → `tests/unit/pieces/Bishop.test.ts` 8. `src/pieces/Rook.ts` → `tests/unit/pieces/Rook.test.ts` 9. `src/pieces/Queen.ts` → `tests/unit/pieces/Queen.test.ts` 10. `src/pieces/King.ts` → `tests/unit/pieces/King.test.ts` **Priority 4: Game Logic** 11. `src/game/GameState.ts` → `tests/unit/game/GameState.test.ts` 12. `src/engine/MoveValidator.ts` → `tests/unit/engine/MoveValidator.test.ts` 13. `src/engine/SpecialMoves.ts` → `tests/unit/engine/SpecialMoves.test.ts` **Priority 5: Controllers & Views** 14. `src/utils/EventBus.ts` → `tests/unit/utils/EventBus.test.ts` 15. `src/views/BoardRenderer.ts` → `tests/unit/views/BoardRenderer.test.ts` 16. `src/controllers/DragDropHandler.ts` → `tests/unit/controllers/DragDropHandler.test.ts` 17. `src/controllers/GameController.ts` → `tests/integration/GameController.test.ts` ### 2.3 Test File Template **Example: King.test.ts Migration** ```typescript import { describe, test, expect, beforeEach } from '@jest/globals'; import { King } from '@pieces/King'; import { Board } from '@game/Board'; import type { Piece, PieceColor, Position } from '@/types'; describe('King', () => { let board: Board; beforeEach(() => { board = new Board(); board.clear(); }); describe('One Square Movement', () => { test('king can move one square in any direction', () => { const king = new King('white', { row: 4, col: 4 }); board.setPiece(4, 4, king); const moves = king.getValidMoves(board); const expectedMoves: Position[] = [ { row: 3, col: 3 }, { row: 3, col: 4 }, { row: 3, col: 5 }, { row: 4, col: 3 }, /* king here */ { row: 4, col: 5 }, { row: 5, col: 3 }, { row: 5, col: 4 }, { row: 5, col: 5 } ]; expect(moves).toHaveLength(8); expectedMoves.forEach((expected: Position) => { expect(moves).toContainEqual(expected); }); }); test('king in corner has 3 moves', () => { const king = new King('white', { row: 0, col: 0 }); board.setPiece(0, 0, king); const moves: Position[] = king.getValidMoves(board); expect(moves).toHaveLength(3); expect(moves).toContainEqual({ row: 0, col: 1 }); expect(moves).toContainEqual({ row: 1, col: 0 }); expect(moves).toContainEqual({ row: 1, col: 1 }); }); }); describe('Cannot Move Into Check', () => { test('king cannot move into attacked square', () => { const whiteKing = new King('white', { row: 7, col: 4 }); const blackRook: Piece = { type: 'rook', color: 'black', position: { row: 0, col: 5 }, hasMoved: false, getValidMoves: jest.fn() }; board.setPiece(7, 4, whiteKing); board.setPiece(0, 5, blackRook); const moves = whiteKing.getValidMoves(board, board); // Type-safe assertions expect(moves).not.toContainEqual({ row: 7, col: 5 }); expect(moves).not.toContainEqual({ row: 6, col: 5 }); }); }); describe('Castling - Kingside', () => { test('king can castle kingside when conditions met', () => { const king = new King('white', { row: 7, col: 4 }); const rook: Piece = { type: 'rook', color: 'white', position: { row: 7, col: 7 }, hasMoved: false, getValidMoves: jest.fn() }; board.setPiece(7, 4, king); board.setPiece(7, 7, rook); const gameState = { castlingRights: { whiteKingside: true } }; const moves = king.getValidMoves(board, board, gameState); expect(moves).toContainEqual({ row: 7, col: 6, castling: 'kingside' }); }); }); }); ``` --- ## Phase 3: Type-Safe Test Utilities ### 3.1 Test Factory Pattern **File:** `tests/utils/factories.ts` ```typescript import { jest } from '@jest/globals'; import type { Piece, PieceType, PieceColor, Position } from '@/types'; export class TestPieceFactory { static createPiece( type: PieceType, color: PieceColor, position: Position, hasMoved: boolean = false ): Piece { return { type, color, position: { ...position }, hasMoved, getValidMoves: jest.fn(() => []) }; } static createPawn(color: PieceColor, position: Position): Piece { return this.createPiece('pawn', color, position); } static createKing(color: PieceColor, position: Position, hasMoved: boolean = false): Piece { return this.createPiece('king', color, position, hasMoved); } static createRook(color: PieceColor, position: Position, hasMoved: boolean = false): Piece { return this.createPiece('rook', 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 createQueen(color: PieceColor, position: Position): Piece { return this.createPiece('queen', color, position); } } export class TestBoardFactory { static createEmptyBoard(): Board { const board = new Board(); board.clear(); return board; } static createStartingPosition(): Board { const board = new Board(); board.setupInitialPosition(); return board; } 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; } } ``` ### 3.2 Type-Safe Mock Helpers **File:** `tests/utils/mocks.ts` ```typescript import { jest } from '@jest/globals'; import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'; import type { Board, GameState, MoveValidator } from '@/types'; export function createMockBoard(): DeepMockProxy { return mockDeep(); } export function createMockGameState(): DeepMockProxy { return mockDeep(); } export function createMockMoveValidator(): DeepMockProxy { return mockDeep(); } // DOM mocks export function createMockElement(tagName: string = 'div'): HTMLElement { const element = document.createElement(tagName); jest.spyOn(element, 'addEventListener'); jest.spyOn(element, 'removeEventListener'); return element; } export function resetAllMocks(...mocks: DeepMockProxy[]): void { mocks.forEach(mock => mockReset(mock)); } ``` ### 3.3 Custom Assertion Helpers **File:** `tests/utils/assertions.ts` ```typescript import type { Position, Move, Piece } from '@/types'; export function expectPositionsEqual(actual: Position, expected: Position): void { expect(actual.row).toBe(expected.row); expect(actual.col).toBe(expected.col); } export function expectMoveInArray(moves: Move[], expected: Move): void { const found = moves.some( move => move.from.row === expected.from.row && move.from.col === expected.from.col && move.to.row === expected.to.row && move.to.col === expected.to.col ); expect(found).toBe(true); } export function expectValidPiece(piece: Piece | null): asserts piece is Piece { expect(piece).not.toBeNull(); expect(piece).toBeDefined(); expect(piece).toHaveProperty('type'); expect(piece).toHaveProperty('color'); expect(piece).toHaveProperty('position'); } export function expectPieceType(piece: Piece, expectedType: PieceType): void { expectValidPiece(piece); expect(piece.type).toBe(expectedType); } ``` --- ## Phase 4: Testing Type Definitions ### 4.1 Type-Level Testing Strategy **File:** `tests/types/type-tests.ts` ```typescript import { expectType, expectError, expectAssignable } from 'tsd'; import type { Piece, PieceType, PieceColor, Position, Move, GameState, CastlingRights } from '@/types'; // Test: Position type is correct expectType({ row: 0, col: 0 }); expectError({ row: -1, col: 0 }); // Should not allow negative expectError({ x: 0, y: 0 }); // Wrong property names // Test: PieceColor is a union of literals expectType('white'); expectType('black'); expectError('red'); // Invalid color // Test: PieceType exhaustiveness expectType('pawn'); expectType('knight'); expectType('bishop'); expectType('rook'); expectType('queen'); expectType('king'); expectError('wizard'); // Invalid type // Test: Piece interface structure expectAssignable({ type: 'pawn', color: 'white', position: { row: 6, col: 4 }, hasMoved: false, getValidMoves: (board) => [] }); // Test: Move type structure expectType({ from: { row: 6, col: 4 }, to: { row: 4, col: 4 }, piece: 'pawn', captured: null, promotion: null, castling: null, enPassant: false }); // Test: CastlingRights structure expectType({ whiteKingside: true, whiteQueenside: true, blackKingside: false, blackQueenside: false }); // Test: GameState interface expectAssignable({ currentTurn: 'white', board: {} as Board, moveHistory: [], capturedPieces: { white: [], black: [] }, castlingRights: { whiteKingside: true, whiteQueenside: true, blackKingside: true, blackQueenside: true }, enPassantTarget: null, halfMoveClock: 0, fullMoveNumber: 1, inCheck: false, isCheckmate: false, isStalemate: false }); ``` ### 4.2 Install Type Testing Dependencies ```bash npm install --save-dev tsd @types/testing-library__jest-dom ``` ### 4.3 Type Test Script **Add to package.json:** ```json { "scripts": { "test:types": "tsd", "test:all": "npm run test:types && npm test" } } ``` --- ## Phase 5: Regression Prevention Strategy ### 5.1 Snapshot Testing **File:** `tests/unit/pieces/__snapshots__/King.test.ts.snap` ```typescript // tests/unit/pieces/King.test.ts describe('King Moves Snapshot', () => { test('king starting position moves should match snapshot', () => { const board = TestBoardFactory.createStartingPosition(); const king = board.getPiece(7, 4) as King; const moves = king.getValidMoves(board); // Ensure consistent ordering for snapshot const sortedMoves = moves.sort((a, b) => a.row === b.row ? a.col - b.col : a.row - b.row ); expect(sortedMoves).toMatchSnapshot(); }); }); ``` ### 5.2 Visual Regression Testing **File:** `tests/visual/board-rendering.test.ts` ```typescript import { BoardRenderer } from '@views/BoardRenderer'; import { Board } from '@game/Board'; describe('Board Visual Regression', () => { let container: HTMLElement; beforeEach(() => { container = document.createElement('div'); container.id = 'board'; document.body.appendChild(container); }); afterEach(() => { document.body.removeChild(container); }); test('starting position renders correctly', () => { const board = new Board(); board.setupInitialPosition(); const renderer = new BoardRenderer(board); renderer.render(); // Snapshot of rendered HTML expect(container.innerHTML).toMatchSnapshot(); }); test('board after e4 move renders correctly', () => { const board = new Board(); board.setupInitialPosition(); board.movePiece(6, 4, 4, 4); // e2 to e4 const renderer = new BoardRenderer(board); renderer.render(); expect(container.innerHTML).toMatchSnapshot(); }); }); ``` ### 5.3 Integration Test Suite **File:** `tests/integration/full-game.test.ts` ```typescript import { GameController } from '@controllers/GameController'; import { Board } from '@game/Board'; import { GameState } from '@game/GameState'; describe('Full Game Integration', () => { let gameController: GameController; beforeEach(() => { document.body.innerHTML = `
`; gameController = new GameController(); }); test('Scholar\'s Mate sequence', () => { const moves = [ { from: { row: 6, col: 4 }, to: { row: 4, col: 4 } }, // e4 { from: { row: 1, col: 4 }, to: { row: 3, col: 4 } }, // e5 { from: { row: 7, col: 5 }, to: { row: 4, col: 2 } }, // Bc4 { from: { row: 0, col: 1 }, to: { row: 2, col: 2 } }, // Nc6 { from: { row: 7, col: 3 }, to: { row: 3, col: 7 } }, // Qh5 { from: { row: 0, col: 6 }, to: { row: 2, col: 5 } }, // Nf6 { from: { row: 3, col: 7 }, to: { row: 1, col: 5 } } // Qxf7# (checkmate) ]; moves.forEach((move, index) => { const result = gameController.makeMove(move.from, move.to); expect(result.success).toBe(true); if (index === moves.length - 1) { expect(gameController.isCheckmate()).toBe(true); } }); }); test('castling both sides', () => { // Setup position for castling const board = gameController.getBoard(); board.clear(); // White pieces for castling const whiteKing = TestPieceFactory.createKing('white', { row: 7, col: 4 }); const whiteRookKingside = TestPieceFactory.createRook('white', { row: 7, col: 7 }); const whiteRookQueenside = TestPieceFactory.createRook('white', { row: 7, col: 0 }); board.setPiece(7, 4, whiteKing); board.setPiece(7, 7, whiteRookKingside); board.setPiece(7, 0, whiteRookQueenside); // Test kingside castling const kingsideCastle = gameController.makeMove( { row: 7, col: 4 }, { row: 7, col: 6 } ); expect(kingsideCastle.success).toBe(true); expect(board.getPiece(7, 6)?.type).toBe('king'); expect(board.getPiece(7, 5)?.type).toBe('rook'); }); }); ``` ### 5.4 Regression Test Checklist **File:** `tests/regression/CHECKLIST.md` ```markdown # Regression Test Checklist Run this checklist after each migration step: ## Basic Functionality - [ ] All 124 original tests pass - [ ] No TypeScript compilation errors - [ ] No type errors in test files - [ ] Test coverage remains >= 80% ## Piece Movement - [ ] All pieces can move correctly - [ ] Capture logic works for all pieces - [ ] Special moves (castling, en passant, promotion) work - [ ] Move validation prevents illegal moves ## Game State - [ ] Turn switching works correctly - [ ] Check detection works - [ ] Checkmate detection works - [ ] Stalemate detection works - [ ] Move history is recorded ## UI Integration - [ ] Board renders correctly - [ ] Drag and drop works - [ ] Piece highlighting works - [ ] Move indicators appear correctly ## Performance - [ ] Tests run in < 10 seconds total - [ ] No memory leaks detected - [ ] No infinite loops in move generation ## Type Safety - [ ] All types are properly exported - [ ] No `any` types in production code - [ ] All function signatures are typed - [ ] Interfaces are properly defined ``` --- ## Phase 6: Migration Execution Plan ### 6.1 Pre-Migration Setup **Step 1: Install Dependencies** ```bash npm install --save-dev typescript ts-jest @types/jest @types/node @jest/globals jest-mock-extended tsd ``` **Step 2: Create TypeScript Config** ```bash # Create tsconfig.json (use Issue #6 spec) # Create jest.config.ts # Migrate tests/setup.js to tests/setup.ts ``` **Step 3: Create Test Utilities** ```bash mkdir -p tests/utils tests/types tests/integration tests/visual # Create factories.ts, mocks.ts, assertions.ts # Create type-tests.ts ``` **Step 4: Verify Jest Configuration** ```bash npm test -- --listTests # Should show existing .test.js files ``` ### 6.2 Migration Workflow (Per File) ```bash # For each source file (e.g., King.js): # 1. Create feature branch git checkout -b migrate/king-typescript # 2. Migrate source file mv js/pieces/King.js src/pieces/King.ts # Add type annotations to King.ts # 3. Migrate test file mv tests/unit/pieces/King.test.js tests/unit/pieces/King.test.ts # Add type annotations to test file # 4. Run specific test npm test -- King.test.ts # 5. Fix any TypeScript errors # Iterate until all tests pass # 6. Run full test suite npm test # 7. Check coverage npm run test:coverage # 8. Run type tests npm run test:types # 9. Commit if successful git add src/pieces/King.ts tests/unit/pieces/King.test.ts git commit -m "feat: migrate King piece to TypeScript - Add type annotations to King class - Migrate tests to TypeScript - All tests passing (124/124) - Coverage maintained at 80%+" # 10. Push and create PR git push origin migrate/king-typescript gh pr create --title "feat: Migrate King piece to TypeScript" \ --body "Part of Issue #6 TypeScript migration" # 11. After PR approval and merge git checkout main git pull origin main ``` ### 6.3 Maintaining Green Tests **Strategy: Never Break Main Branch** 1. **Feature Branches:** Each file migration is a separate branch 2. **Test-First Verification:** Tests must pass before committing 3. **Incremental Merges:** Merge one file pair at a time 4. **Automated CI Checks:** All tests run on every PR 5. **Rollback Plan:** Keep JS files in separate branch until migration complete **Parallel File Support (Transition Period):** ```typescript // jest.config.ts - Support both .js and .ts testMatch: [ '**/tests/**/*.test.ts', '**/tests/**/*.test.js' // Remove after full migration ], transform: { '^.+\\.tsx?$': 'ts-jest', '^.+\\.jsx?$': 'babel-jest' } ``` --- ## Phase 7: Type Coverage Metrics ### 7.1 Install Type Coverage Tool ```bash npm install --save-dev type-coverage ``` ### 7.2 Configure Type Coverage **Add to package.json:** ```json { "scripts": { "type-check": "tsc --noEmit", "type-coverage": "type-coverage --at-least 90 --strict", "test:types:all": "npm run type-check && npm run type-coverage && npm run test:types" }, "typeCoverage": { "atLeast": 90, "strict": true, "ignoreCatch": false, "ignoreFiles": [ "**/*.test.ts", "**/*.spec.ts", "**/node_modules/**" ] } } ``` ### 7.3 Type Coverage Goals | Metric | Target | Critical | |--------|--------|----------| | Overall Type Coverage | 90% | Yes | | Strict Type Coverage | 85% | Yes | | Any Types | < 5% | Yes | | Implicit Any | 0% | Yes | | Untyped Functions | < 10% | No | ### 7.4 CI Integration **File:** `.github/workflows/typescript-tests.yml` ```yaml name: TypeScript Tests on: pull_request: branches: [main] push: branches: [main] jobs: type-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - run: npm ci - run: npm run type-check - run: npm run type-coverage unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - run: npm ci - run: npm test - run: npm run test:coverage - uses: codecov/codecov-action@v3 with: file: ./coverage/coverage-final.json type-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - run: npm ci - run: npm run test:types ``` --- ## Phase 8: E2E Test Compatibility ### 8.1 E2E Testing Strategy **Current State:** No E2E tests exist **Recommendation:** Add Playwright for E2E tests ```bash npm install --save-dev @playwright/test ``` ### 8.2 Playwright Configuration **File:** `playwright.config.ts` ```typescript import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests/e2e', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { baseURL: 'http://localhost:8080', trace: 'on-first-retry', screenshot: 'only-on-failure' }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } } ], webServer: { command: 'npm run dev', url: 'http://localhost:8080', reuseExistingServer: !process.env.CI } }); ``` ### 8.3 Sample E2E Tests **File:** `tests/e2e/gameplay.spec.ts` ```typescript import { test, expect } from '@playwright/test'; test.describe('Chess Game E2E', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); }); test('should load game with initial position', async ({ page }) => { // Check board is rendered const board = page.locator('#board'); await expect(board).toBeVisible(); // Check all pieces are present const pieces = page.locator('.piece'); await expect(pieces).toHaveCount(32); // Check turn indicator const turnIndicator = page.locator('#turn-indicator'); await expect(turnIndicator).toContainText("White's turn"); }); test('should allow valid pawn move', async ({ page }) => { // Drag white pawn from e2 to e4 const e2Pawn = page.locator('[data-position="6-4"]'); const e4Square = page.locator('[data-square="4-4"]'); await e2Pawn.dragTo(e4Square); // Verify move was made await expect(e4Square.locator('.piece')).toBeVisible(); await expect(e4Square.locator('.piece')).toHaveAttribute('data-piece', 'pawn'); // Verify turn switched const turnIndicator = page.locator('#turn-indicator'); await expect(turnIndicator).toContainText("Black's turn"); }); test('should prevent invalid move', async ({ page }) => { // Try to move white pawn from e2 to e5 (invalid) const e2Pawn = page.locator('[data-position="6-4"]'); const e5Square = page.locator('[data-square="3-4"]'); await e2Pawn.dragTo(e5Square); // Verify move was rejected await expect(e5Square.locator('.piece')).not.toBeVisible(); // Verify pawn is still at e2 const e2Square = page.locator('[data-square="6-4"]'); await expect(e2Square.locator('.piece')).toBeVisible(); // Turn should still be white const turnIndicator = page.locator('#turn-indicator'); await expect(turnIndicator).toContainText("White's turn"); }); test('should detect checkmate', async ({ page }) => { // Play Scholar's Mate using page.evaluate to directly manipulate game state await page.evaluate(() => { const moves = [ { from: { row: 6, col: 4 }, to: { row: 4, col: 4 } }, // e4 { from: { row: 1, col: 4 }, to: { row: 3, col: 4 } }, // e5 { from: { row: 7, col: 5 }, to: { row: 4, col: 2 } }, // Bc4 { from: { row: 0, col: 1 }, to: { row: 2, col: 2 } }, // Nc6 { from: { row: 7, col: 3 }, to: { row: 3, col: 7 } }, // Qh5 { from: { row: 0, col: 6 }, to: { row: 2, col: 5 } }, // Nf6 { from: { row: 3, col: 7 }, to: { row: 1, col: 5 } } // Qxf7# checkmate ]; moves.forEach(move => { (window as any).gameController.makeMove(move.from, move.to); }); }); // Check for checkmate message const gameMessage = page.locator('#game-message'); await expect(gameMessage).toContainText('Checkmate'); await expect(gameMessage).toContainText('White wins'); }); }); ``` **Add to package.json:** ```json { "scripts": { "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:debug": "playwright test --debug" } } ``` --- ## Success Metrics & Goals ### Test Coverage Goals | Metric | Current | Target | Critical | |--------|---------|--------|----------| | Total Tests | 124 | 150+ | Yes | | Test Pass Rate | 100% | 100% | Yes | | Code Coverage | ~80% | 85% | Yes | | Type Coverage | 0% | 90% | Yes | | Branch Coverage | ~75% | 80% | No | ### Quality Gates **Before Any PR Merge:** 1. ✅ All tests pass (124/124 minimum) 2. ✅ No TypeScript compilation errors 3. ✅ No type errors (strict mode) 4. ✅ Code coverage >= 80% 5. ✅ Type coverage >= 90% 6. ✅ No `any` types without explicit justification 7. ✅ All ESLint rules pass 8. ✅ Prettier formatting applied **Before Final Migration Complete:** 1. ✅ All 17 source files migrated 2. ✅ All 7+ test files migrated 3. ✅ E2E tests implemented and passing 4. ✅ Type tests passing 5. ✅ Visual regression tests passing 6. ✅ Integration tests passing 7. ✅ CI/CD pipeline green 8. ✅ Documentation updated ### Performance Goals | Metric | Target | |--------|--------| | Unit Test Suite | < 10 seconds | | Integration Tests | < 5 seconds | | E2E Tests | < 30 seconds | | Type Check | < 5 seconds | | Total CI Time | < 2 minutes | --- ## Risk Mitigation ### Risk 1: Test Failures During Migration **Mitigation:** - One file at a time approach - Keep JS files until full migration - Automated rollback via Git - Comprehensive regression tests ### Risk 2: Type Errors Breaking Tests **Mitigation:** - Use `diagnostics.warnOnly` during transition - Incremental strict mode adoption - Type-safe test utilities from day 1 - Extensive type testing ### Risk 3: Performance Degradation **Mitigation:** - Benchmark tests before/after - Use isolated modules for faster compilation - Cache type checking results - Monitor CI execution time ### Risk 4: Coverage Drop **Mitigation:** - Enforce coverage thresholds in CI - Track coverage per-file - Add tests for edge cases revealed by types - Visual coverage reports --- ## Timeline & Milestones ### Week 1: Setup & Foundation - Day 1-2: Install dependencies, configure Jest + TypeScript - Day 3: Migrate test utilities and setup files - Day 4-5: Create factories, mocks, and assertions - Milestone: Green test suite with TypeScript configuration ### Week 2: Core Types & Base Classes - Day 1: Migrate Constants and Helpers + tests - Day 2-3: Migrate Piece base class + tests - Day 4-5: Migrate Board class + tests - Milestone: Core infrastructure in TypeScript ### Week 3: Piece Implementations - Day 1: Pawn + tests - Day 2: Knight & Bishop + tests - Day 3: Rook & Queen + tests - Day 4-5: King + tests (complex, castling logic) - Milestone: All pieces in TypeScript ### Week 4: Game Logic & Engine - Day 1-2: GameState + MoveValidator + tests - Day 3: SpecialMoves + tests - Day 4-5: Integration tests - Milestone: Complete game logic in TypeScript ### Week 5: Controllers, Views & E2E - Day 1: EventBus + tests - Day 2-3: BoardRenderer + DragDropHandler + tests - Day 4: GameController + integration tests - Day 5: E2E tests with Playwright - Milestone: Full application in TypeScript ### Week 6: Polish & Verification - Day 1-2: Type coverage optimization (reach 90%) - Day 3: Visual regression tests - Day 4: Performance benchmarking - Day 5: Final verification, documentation - Milestone: Migration complete, all quality gates passed --- ## Conclusion This comprehensive testing strategy ensures: 1. **Zero Regression:** All 124 tests remain passing throughout migration 2. **Type Safety:** 90%+ type coverage with strict mode enabled 3. **Maintainability:** Type-safe test utilities and factories 4. **Confidence:** Multi-layer testing (unit, integration, E2E, type-level) 5. **Quality:** Automated CI checks enforce quality gates 6. **Speed:** Incremental migration allows continuous deployment **Next Steps:** 1. Review and approve this strategy 2. Set up initial TypeScript + Jest configuration 3. Begin migration with Constants/Helpers (lowest risk) 4. Establish CI pipeline 5. Execute migration plan file-by-file **Success Criteria:** - ✅ 100% test pass rate maintained - ✅ 90%+ type coverage achieved - ✅ Zero production bugs introduced - ✅ CI pipeline running in < 2 minutes - ✅ All quality gates passing on every PR