strat-gameplay-webapp/frontend-sba/tests/unit/composables/useGameActions.spec.ts
Cal Corum 4e7ea9e514 CLAUDE: Remove alignment field from frontend - complete Session 1 cleanup
Removed all references to the defensive alignment field across frontend codebase
after backend removal in Session 1. The alignment field was determined to be unused
and was removed from DefensiveDecision model.

Changes:
- types/websocket.ts: Removed alignment from DefensiveDecisionRequest interface
- composables/useGameActions.ts: Removed alignment from submit handler
- pages/demo-decisions.vue: Updated demo state and summary text (alignment → depths)
- pages/games/[id].vue: Updated decision history text for both defensive and offensive
  * Defensive: Now shows "infield depth, outfield depth" instead of "alignment, infield"
  * Offensive: Updated to use new action field with proper labels (swing_away, hit_and_run, etc.)
- Test files (3): Updated all test cases to remove alignment references
  * tests/unit/composables/useGameActions.spec.ts
  * tests/unit/store/game-decisions.spec.ts
  * tests/unit/components/Decisions/DefensiveSetup.spec.ts

Also updated offensive decision handling to match Session 2 changes (approach/hit_and_run/bunt_attempt → action field).

Total: 7 files modified, all alignment references removed
Verified: Zero remaining alignment references in .ts/.vue/.js files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 15:34:59 -06:00

441 lines
13 KiB
TypeScript

/**
* 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
;(globalThis as any).process = {
...((globalThis as any).process || {}),
env: {
...((globalThis as any).process?.env || {}),
NODE_ENV: 'test',
},
client: true,
}
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'
// 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(() => {
// 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()
})
describe('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')
})
})
describe('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',
})
})
})
describe('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')
})
})
})