import { describe, it, expect, vi, beforeEach } from 'vitest'; import axios from 'axios'; import { makeMove, isPromotionRequired, getPromotionRequirement, createPromotionMove, type GameResponse, type PromotionRequirement, type GameState } from './api'; // Mock axios vi.mock('axios'); const mockedAxios = vi.mocked(axios); describe('API Service - Promotion Support', () => { beforeEach(() => { vi.clearAllMocks(); }); const mockGameState: GameState = { fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', turn: 'white', game_state: 'active', legal_moves: ['e2e4', 'e2e3'], in_check: false, move_count: 1 }; describe('makeMove function', () => { it('should make a normal move successfully', async () => { const mockResponse: GameResponse = { status: 'success', board_state: mockGameState, player_move: 'e2e4' }; mockedAxios.post.mockResolvedValueOnce({ data: mockResponse }); const result = await makeMove('e2e4'); expect(mockedAxios.post).toHaveBeenCalledWith('/api/game/move', { move: 'e2e4' }); expect(result).toEqual(mockResponse); }); it('should make a promotion move with piece specified', async () => { const mockResponse: GameResponse = { status: 'success', board_state: mockGameState, player_move: 'e7e8q' }; mockedAxios.post.mockResolvedValueOnce({ data: mockResponse }); const result = await makeMove('e7e8', 'queen'); expect(mockedAxios.post).toHaveBeenCalledWith('/api/game/move', { move: 'e7e8q' }); expect(result).toEqual(mockResponse); }); it('should handle promotion required error response', async () => { const errorResponse = { status: 'promotion_required', message: 'Pawn promotion requires piece selection', board_state: mockGameState, promotion_details: { from: 'e7', to: 'e8', available_pieces: ['queen', 'rook', 'bishop', 'knight'] } }; const axiosError = { isAxiosError: true, response: { status: 400, data: errorResponse } }; mockedAxios.post.mockRejectedValueOnce(axiosError); mockedAxios.isAxiosError.mockReturnValueOnce(true); const result = await makeMove('e7e8'); expect(result.status).toBe('promotion_required'); expect(result.promotion_required).toEqual({ from: 'e7', to: 'e8', available_pieces: ['queen', 'rook', 'bishop', 'knight'] }); }); it('should handle promotion required error with minimal data', async () => { const errorResponse = { status: 'promotion_required', message: 'Pawn promotion requires piece selection' }; const axiosError = { isAxiosError: true, response: { status: 400, data: errorResponse } }; mockedAxios.post.mockRejectedValueOnce(axiosError); mockedAxios.isAxiosError.mockReturnValueOnce(true); const result = await makeMove('e7e8'); expect(result.status).toBe('promotion_required'); expect(result.promotion_required).toEqual({ from: 'e7', to: 'e8', available_pieces: ['queen', 'rook', 'bishop', 'knight'] }); }); it('should re-throw non-promotion errors', async () => { const axiosError = { isAxiosError: true, response: { status: 500, data: { message: 'Internal server error' } } }; mockedAxios.post.mockRejectedValueOnce(axiosError); mockedAxios.isAxiosError.mockReturnValueOnce(true); await expect(makeMove('e2e4')).rejects.toEqual(axiosError); }); it('should re-throw non-axios errors', async () => { const networkError = new Error('Network error'); mockedAxios.post.mockRejectedValueOnce(networkError); mockedAxios.isAxiosError.mockReturnValueOnce(false); await expect(makeMove('e2e4')).rejects.toEqual(networkError); }); it('should handle different promotion pieces correctly', async () => { const testCases = [ { piece: 'queen', expected: 'e7e8q' }, { piece: 'rook', expected: 'e7e8r' }, { piece: 'bishop', expected: 'e7e8b' }, { piece: 'knight', expected: 'e7e8n' } ] as const; for (const testCase of testCases) { const mockResponse: GameResponse = { status: 'success', board_state: mockGameState, player_move: testCase.expected }; mockedAxios.post.mockResolvedValueOnce({ data: mockResponse }); await makeMove('e7e8', testCase.piece); expect(mockedAxios.post).toHaveBeenCalledWith('/api/game/move', { move: testCase.expected }); } }); }); describe('isPromotionRequired function', () => { it('should return true when promotion is required', () => { const response: GameResponse = { status: 'promotion_required', board_state: mockGameState, promotion_required: { from: 'e7', to: 'e8', available_pieces: ['queen', 'rook', 'bishop', 'knight'] } }; expect(isPromotionRequired(response)).toBe(true); }); it('should return false when promotion is not required', () => { const response: GameResponse = { status: 'success', board_state: mockGameState }; expect(isPromotionRequired(response)).toBe(false); }); it('should return false when status is promotion_required but no promotion_required field', () => { const response: GameResponse = { status: 'promotion_required', board_state: mockGameState }; expect(isPromotionRequired(response)).toBe(false); }); }); describe('getPromotionRequirement function', () => { it('should return promotion requirement when present', () => { const promotionReq: PromotionRequirement = { from: 'e7', to: 'e8', available_pieces: ['queen', 'rook', 'bishop', 'knight'] }; const response: GameResponse = { status: 'promotion_required', board_state: mockGameState, promotion_required: promotionReq }; expect(getPromotionRequirement(response)).toEqual(promotionReq); }); it('should return null when no promotion requirement', () => { const response: GameResponse = { status: 'success', board_state: mockGameState }; expect(getPromotionRequirement(response)).toBeNull(); }); }); describe('createPromotionMove function', () => { it('should create correct UCI notation for different pieces', () => { expect(createPromotionMove('e7', 'e8', 'queen')).toBe('e7e8q'); expect(createPromotionMove('e7', 'e8', 'rook')).toBe('e7e8r'); expect(createPromotionMove('e7', 'e8', 'bishop')).toBe('e7e8b'); expect(createPromotionMove('e7', 'e8', 'knight')).toBe('e7e8n'); }); it('should handle different squares correctly', () => { expect(createPromotionMove('a7', 'a8', 'queen')).toBe('a7a8q'); expect(createPromotionMove('h2', 'h1', 'rook')).toBe('h2h1r'); expect(createPromotionMove('d7', 'd8', 'bishop')).toBe('d7d8b'); }); }); describe('Integration scenarios', () => { it('should handle complete promotion flow', async () => { // First call - promotion required const promotionRequiredError = { isAxiosError: true, response: { status: 400, data: { status: 'promotion_required', message: 'Pawn promotion requires piece selection', promotion_details: { from: 'e7', to: 'e8', available_pieces: ['queen', 'rook', 'bishop', 'knight'] } } } }; mockedAxios.post.mockRejectedValueOnce(promotionRequiredError); mockedAxios.isAxiosError.mockReturnValueOnce(true); const firstResult = await makeMove('e7e8'); expect(isPromotionRequired(firstResult)).toBe(true); const promotionReq = getPromotionRequirement(firstResult); expect(promotionReq).toBeTruthy(); expect(promotionReq!.available_pieces).toContain('queen'); // Second call - with promotion piece const successResponse: GameResponse = { status: 'success', board_state: mockGameState, player_move: 'e7e8q' }; mockedAxios.post.mockResolvedValueOnce({ data: successResponse }); const secondResult = await makeMove('e7e8', 'queen'); expect(secondResult.status).toBe('success'); expect(secondResult.player_move).toBe('e7e8q'); }); }); });