/** * Game Actions Composable Tests * * Tests for type-safe game action emitters with validation and error handling. */ // IMPORTANT: Mock process.env BEFORE any other imports to fix Pinia import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' import { ref, computed } from 'vue' import { useGameActions } from '~/composables/useGameActions' import type { DefensiveDecision, OffensiveDecision } from '~/types' ;(globalThis as any).process = { ...((globalThis as any).process || {}), env: { ...((globalThis as any).process?.env || {}), NODE_ENV: 'test', }, client: true, } // Mock composables vi.mock('~/composables/useWebSocket', () => ({ useWebSocket: vi.fn(), })) vi.mock('~/store/game', () => ({ useGameStore: vi.fn(), })) vi.mock('~/store/ui', () => ({ useUiStore: vi.fn(), })) describe('useGameActions', () => { let mockSocket: any let mockGameStore: any let mockUiStore: any beforeEach(async () => { // Mock socket with emit function mockSocket = { value: { emit: vi.fn(), }, } // Mock game store mockGameStore = { gameId: 'game-123', canRollDice: true, canSubmitOutcome: true, resetGame: vi.fn(), } // Mock UI store mockUiStore = { showError: vi.fn(), showInfo: vi.fn(), showWarning: vi.fn(), } // Set up mocks const { useWebSocket } = await import('~/composables/useWebSocket') const { useGameStore } = await import('~/store/game') const { useUiStore } = await import('~/store/ui') vi.mocked(useWebSocket).mockReturnValue({ socket: mockSocket, isConnected: ref(true), isConnecting: ref(false), connectionError: ref(null), canConnect: computed(() => true), connect: vi.fn(), disconnect: vi.fn(), }) vi.mocked(useGameStore).mockReturnValue(mockGameStore) vi.mocked(useUiStore).mockReturnValue(mockUiStore) }) afterEach(() => { vi.clearAllMocks() }) // TODO: Fix require() module import issues describe.skip('validation', () => { it('validates connection before emitting', () => { const { useWebSocket } = require('~/composables/useWebSocket') vi.mocked(useWebSocket).mockReturnValue({ socket: mockSocket, isConnected: ref(false), // Not connected isConnecting: ref(false), connectionError: ref(null), canConnect: computed(() => false), connect: vi.fn(), disconnect: vi.fn(), }) const actions = useGameActions() actions.joinGame() expect(mockSocket.value.emit).not.toHaveBeenCalled() expect(mockUiStore.showError).toHaveBeenCalledWith('Not connected to game server') }) it('validates socket exists before emitting', () => { const { useWebSocket } = require('~/composables/useWebSocket') vi.mocked(useWebSocket).mockReturnValue({ socket: ref(null), // No socket isConnected: ref(true), isConnecting: ref(false), connectionError: ref(null), canConnect: computed(() => true), connect: vi.fn(), disconnect: vi.fn(), }) const actions = useGameActions() actions.joinGame() expect(mockUiStore.showError).toHaveBeenCalledWith('WebSocket not initialized') }) it('validates gameId exists before emitting', () => { mockGameStore.gameId = null // No game ID const actions = useGameActions() actions.joinGame() expect(mockSocket.value.emit).not.toHaveBeenCalled() expect(mockUiStore.showError).toHaveBeenCalledWith('No active game') }) it('validates canRollDice before rolling dice', () => { mockGameStore.canRollDice = false const actions = useGameActions() actions.rollDice() expect(mockSocket.value.emit).not.toHaveBeenCalled() expect(mockUiStore.showWarning).toHaveBeenCalledWith('Cannot roll dice at this time') }) it('validates canSubmitOutcome before submitting outcome', () => { mockGameStore.canSubmitOutcome = false const actions = useGameActions() actions.submitManualOutcome('SINGLE_1') expect(mockSocket.value.emit).not.toHaveBeenCalled() expect(mockUiStore.showWarning).toHaveBeenCalledWith('Must roll dice first') }) }) // TODO: Fix graceful handling test describe.skip('connection actions', () => { it('emits join_game with correct parameters', () => { const actions = useGameActions() actions.joinGame('player') expect(mockSocket.value.emit).toHaveBeenCalledWith('join_game', { game_id: 'game-123', role: 'player', }) }) it('emits join_game as spectator', () => { const actions = useGameActions() actions.joinGame('spectator') expect(mockSocket.value.emit).toHaveBeenCalledWith('join_game', { game_id: 'game-123', role: 'spectator', }) }) it('emits leave_game and resets game store', () => { const actions = useGameActions() actions.leaveGame() expect(mockSocket.value.emit).toHaveBeenCalledWith('leave_game', { game_id: 'game-123', }) expect(mockGameStore.resetGame).toHaveBeenCalled() }) it('handles leave_game when not connected gracefully', () => { const { useWebSocket } = require('~/composables/useWebSocket') vi.mocked(useWebSocket).mockReturnValue({ socket: ref(null), isConnected: ref(false), isConnecting: ref(false), connectionError: ref(null), canConnect: computed(() => false), connect: vi.fn(), disconnect: vi.fn(), }) const actions = useGameActions() // Should not crash expect(() => actions.leaveGame()).not.toThrow() }) }) describe('strategic decision actions', () => { it('emits defensive decision with correct parameters', () => { const decision: DefensiveDecision = { infield_depth: 'normal', outfield_depth: 'normal', hold_runners: [], } const actions = useGameActions() actions.submitDefensiveDecision(decision) expect(mockSocket.value.emit).toHaveBeenCalledWith('submit_defensive_decision', { game_id: 'game-123', infield_depth: 'normal', outfield_depth: 'normal', hold_runners: [], }) }) it('emits offensive decision with correct parameters', () => { const decision: OffensiveDecision = { action: 'steal', steal_attempts: [2], } const actions = useGameActions() actions.submitOffensiveDecision(decision) expect(mockSocket.value.emit).toHaveBeenCalledWith('submit_offensive_decision', { game_id: 'game-123', action: 'steal', steal_attempts: [2], }) }) }) describe('manual outcome workflow', () => { it('emits roll_dice and shows info toast', () => { const actions = useGameActions() actions.rollDice() expect(mockSocket.value.emit).toHaveBeenCalledWith('roll_dice', { game_id: 'game-123', }) expect(mockUiStore.showInfo).toHaveBeenCalledWith('Rolling dice...', 2000) }) it('emits submit_manual_outcome with outcome only', () => { const actions = useGameActions() actions.submitManualOutcome('STRIKEOUT') expect(mockSocket.value.emit).toHaveBeenCalledWith('submit_manual_outcome', { game_id: 'game-123', outcome: 'STRIKEOUT', hit_location: undefined, }) }) it('emits submit_manual_outcome with outcome and hit location', () => { const actions = useGameActions() actions.submitManualOutcome('SINGLE_1', '8') expect(mockSocket.value.emit).toHaveBeenCalledWith('submit_manual_outcome', { game_id: 'game-123', outcome: 'SINGLE_1', hit_location: '8', }) }) }) describe('substitution actions', () => { it('emits request_pinch_hitter with correct parameters', () => { const actions = useGameActions() actions.requestPinchHitter(10, 20, 1) expect(mockSocket.value.emit).toHaveBeenCalledWith('request_pinch_hitter', { game_id: 'game-123', player_out_lineup_id: 10, player_in_card_id: 20, team_id: 1, }) expect(mockUiStore.showInfo).toHaveBeenCalledWith('Requesting pinch hitter...', 3000) }) it('emits request_defensive_replacement with correct parameters', () => { const actions = useGameActions() actions.requestDefensiveReplacement(10, 20, 'SS', 1) expect(mockSocket.value.emit).toHaveBeenCalledWith('request_defensive_replacement', { game_id: 'game-123', player_out_lineup_id: 10, player_in_card_id: 20, new_position: 'SS', team_id: 1, }) expect(mockUiStore.showInfo).toHaveBeenCalledWith('Requesting defensive replacement...', 3000) }) it('emits request_pitching_change with correct parameters', () => { const actions = useGameActions() actions.requestPitchingChange(10, 20, 1) expect(mockSocket.value.emit).toHaveBeenCalledWith('request_pitching_change', { game_id: 'game-123', player_out_lineup_id: 10, player_in_card_id: 20, team_id: 1, }) expect(mockUiStore.showInfo).toHaveBeenCalledWith('Requesting pitching change...', 3000) }) }) describe('data request actions', () => { it('emits get_lineup with correct team ID', () => { const actions = useGameActions() actions.getLineup(1) expect(mockSocket.value.emit).toHaveBeenCalledWith('get_lineup', { game_id: 'game-123', team_id: 1, }) }) it('emits get_box_score request', () => { const actions = useGameActions() actions.getBoxScore() expect(mockSocket.value.emit).toHaveBeenCalledWith('get_box_score', { game_id: 'game-123', }) }) it('emits request_game_state and shows info', () => { const actions = useGameActions() actions.requestGameState() expect(mockSocket.value.emit).toHaveBeenCalledWith('request_game_state', { game_id: 'game-123', }) expect(mockUiStore.showInfo).toHaveBeenCalledWith('Syncing game state...', 3000) }) }) describe('gameId override', () => { it('uses provided gameId instead of store gameId', () => { const actions = useGameActions('override-game-456') actions.joinGame() expect(mockSocket.value.emit).toHaveBeenCalledWith('join_game', { game_id: 'override-game-456', role: 'player', }) }) it('falls back to store gameId when not provided', () => { const actions = useGameActions() actions.joinGame() expect(mockSocket.value.emit).toHaveBeenCalledWith('join_game', { game_id: 'game-123', // From store role: 'player', }) }) }) // TODO: Fix require() module import in error message test describe.skip('error handling', () => { it('does not emit when validation fails', () => { mockGameStore.gameId = null const actions = useGameActions() actions.rollDice() actions.submitDefensiveDecision({ infield_depth: 'normal', outfield_depth: 'normal', hold_runners: [], }) actions.getLineup(1) // Should not have emitted any events expect(mockSocket.value.emit).not.toHaveBeenCalled() // Should have shown errors expect(mockUiStore.showError).toHaveBeenCalledTimes(3) }) it('shows appropriate error messages for each validation failure', () => { const { useWebSocket } = require('~/composables/useWebSocket') // Test not connected vi.mocked(useWebSocket).mockReturnValue({ socket: mockSocket, isConnected: ref(false), isConnecting: ref(false), connectionError: ref(null), canConnect: computed(() => false), connect: vi.fn(), disconnect: vi.fn(), }) const actions1 = useGameActions() actions1.joinGame() expect(mockUiStore.showError).toHaveBeenCalledWith('Not connected to game server') vi.clearAllMocks() // Test no socket vi.mocked(useWebSocket).mockReturnValue({ socket: ref(null), isConnected: ref(true), isConnecting: ref(false), connectionError: ref(null), canConnect: computed(() => true), connect: vi.fn(), disconnect: vi.fn(), }) const actions2 = useGameActions() actions2.joinGame() expect(mockUiStore.showError).toHaveBeenCalledWith('WebSocket not initialized') vi.clearAllMocks() // Test no gameId vi.mocked(useWebSocket).mockReturnValue({ socket: mockSocket, isConnected: ref(true), isConnecting: ref(false), connectionError: ref(null), canConnect: computed(() => true), connect: vi.fn(), disconnect: vi.fn(), }) mockGameStore.gameId = null const actions3 = useGameActions() actions3.joinGame() expect(mockUiStore.showError).toHaveBeenCalledWith('No active game') }) }) })