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>
441 lines
13 KiB
TypeScript
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')
|
|
})
|
|
})
|
|
})
|