strat-gameplay-webapp/frontend-sba/tests/unit/components/Substitutions/PinchHitterSelector.spec.ts
Cal Corum eab61ad966 CLAUDE: Phases 3.5, F1-F5 Complete - Statistics & Frontend Components
This commit captures work from multiple sessions building the statistics
system and frontend component library.

Backend - Phase 3.5: Statistics System
- Box score statistics with materialized views
- Play stat calculator for real-time updates
- Stat view refresher service
- Alembic migration for materialized views
- Test coverage: 41 new tests (all passing)

Frontend - Phase F1: Foundation
- Composables: useGameState, useGameActions, useWebSocket
- Type definitions and interfaces
- Store setup with Pinia

Frontend - Phase F2: Game Display
- ScoreBoard, GameBoard, CurrentSituation, PlayByPlay components
- Demo page at /demo

Frontend - Phase F3: Decision Inputs
- DefensiveSetup, OffensiveApproach, StolenBaseInputs components
- DecisionPanel orchestration
- Demo page at /demo-decisions
- Test coverage: 213 tests passing

Frontend - Phase F4: Dice & Manual Outcome
- DiceRoller component
- ManualOutcomeEntry with validation
- PlayResult display
- GameplayPanel orchestration
- Demo page at /demo-gameplay
- Test coverage: 119 tests passing

Frontend - Phase F5: Substitutions
- PinchHitterSelector, DefensiveReplacementSelector, PitchingChangeSelector
- SubstitutionPanel with tab navigation
- Demo page at /demo-substitutions
- Test coverage: 114 tests passing

Documentation:
- PHASE_3_5_HANDOFF.md - Statistics system handoff
- PHASE_F2_COMPLETE.md - Game display completion
- Frontend phase planning docs
- NEXT_SESSION.md updated for Phase F6

Configuration:
- Package updates (Nuxt 4 fixes)
- Tailwind config enhancements
- Game store updates

Test Status:
- Backend: 731/731 passing (100%)
- Frontend: 446/446 passing (100%)
- Total: 1,177 tests passing

Next Phase: F6 - Integration (wire all components into game page)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 09:52:30 -06:00

410 lines
12 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import PinchHitterSelector from '~/components/Substitutions/PinchHitterSelector.vue'
import type { Lineup, SbaPlayer } from '~/types'
// Helper to create mock player
function createMockPlayer(id: number, name: string, pos1?: string): SbaPlayer {
return {
id,
name,
image: `player-${id}.jpg`,
pos_1: pos1 || 'OF',
pos_2: null,
pos_3: null,
pos_4: null,
pos_5: null,
pos_6: null,
pos_7: null,
pos_8: null,
wara: 2.5,
team_id: 1,
team_name: 'Test Team',
season: '2024',
strat_code: null,
bbref_id: null,
injury_rating: null,
}
}
// Helper to create mock lineup entry
function createMockLineup(id: number, player: SbaPlayer, isActive: boolean, isFatigued = false): Lineup {
return {
id,
game_id: 'test-game-123',
team_id: 1,
player_id: player.id,
position: 'OF',
batting_order: isActive ? 1 : null,
is_starter: false,
is_active: isActive,
entered_inning: 1,
replacing_id: null,
after_play: null,
is_fatigued: isFatigued,
player,
}
}
describe('PinchHitterSelector', () => {
const mockBatter = createMockLineup(
1,
createMockPlayer(101, 'John Batter'),
true
)
const mockBenchPlayers = [
createMockLineup(2, createMockPlayer(201, 'Bench Player 1'), false),
createMockLineup(3, createMockPlayer(202, 'Bench Player 2'), false),
createMockLineup(4, createMockPlayer(203, 'Bench Player 3'), false),
]
const defaultProps = {
playerOut: mockBatter,
benchPlayers: mockBenchPlayers,
teamId: 1,
}
describe('Rendering', () => {
it('renders component with header', () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
expect(wrapper.text()).toContain('Pinch Hitter')
})
it('displays current batter information', () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
expect(wrapper.text()).toContain('Current Batter:')
expect(wrapper.text()).toContain('John Batter')
})
it('shows descriptive text with batter name', () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
expect(wrapper.text()).toContain('Select a player from the bench to bat in place of John Batter')
})
it('renders available bench players', () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
expect(wrapper.text()).toContain('Bench Player 1')
expect(wrapper.text()).toContain('Bench Player 2')
expect(wrapper.text()).toContain('Bench Player 3')
})
it('renders action buttons', () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
expect(wrapper.text()).toContain('Cancel')
expect(wrapper.text()).toContain('Substitute Player')
})
})
describe('Player Filtering', () => {
it('filters out active players from bench list', () => {
const mixedPlayers = [
createMockLineup(2, createMockPlayer(201, 'Active Player'), true),
createMockLineup(3, createMockPlayer(202, 'Bench Player 1'), false),
createMockLineup(4, createMockPlayer(203, 'Bench Player 2'), false),
]
const wrapper = mount(PinchHitterSelector, {
props: {
...defaultProps,
benchPlayers: mixedPlayers,
},
})
expect(wrapper.text()).not.toContain('Active Player')
expect(wrapper.text()).toContain('Bench Player 1')
expect(wrapper.text()).toContain('Bench Player 2')
})
it('filters out fatigued players from bench list', () => {
const mixedPlayers = [
createMockLineup(2, createMockPlayer(201, 'Fatigued Player'), false, true),
createMockLineup(3, createMockPlayer(202, 'Available Player'), false),
]
const wrapper = mount(PinchHitterSelector, {
props: {
...defaultProps,
benchPlayers: mixedPlayers,
},
})
expect(wrapper.text()).not.toContain('Fatigued Player')
expect(wrapper.text()).toContain('Available Player')
})
it('shows no players message when no eligible bench players', () => {
const wrapper = mount(PinchHitterSelector, {
props: {
...defaultProps,
benchPlayers: [],
},
})
expect(wrapper.text()).toContain('No eligible bench players available')
})
})
describe('Player Selection', () => {
it('allows selecting a bench player', async () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
const playerCards = wrapper.findAll('.bench-player-card')
await playerCards[0].trigger('click')
expect(wrapper.vm.selectedPlayerId).toBe(201)
})
it('highlights selected player', async () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
const playerCards = wrapper.findAll('.bench-player-card')
await playerCards[0].trigger('click')
expect(playerCards[0].classes()).toContain('bench-player-selected')
})
it('shows checkmark icon on selected player', async () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
const playerCards = wrapper.findAll('.bench-player-card')
await playerCards[0].trigger('click')
await wrapper.vm.$nextTick()
const selectedCard = playerCards[0]
expect(selectedCard.find('.selected-indicator').exists()).toBe(true)
})
it('allows changing selection to different player', async () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
const playerCards = wrapper.findAll('.bench-player-card')
await playerCards[0].trigger('click')
expect(wrapper.vm.selectedPlayerId).toBe(201)
await playerCards[1].trigger('click')
expect(wrapper.vm.selectedPlayerId).toBe(202)
})
})
describe('Submit Validation', () => {
it('disables submit button when no player selected', () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
const submitButton = wrapper.findAll('button').find(btn => btn.text().includes('Substitute Player'))
expect(submitButton?.attributes('disabled')).toBeDefined()
})
it('enables submit button when player selected', async () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
const playerCards = wrapper.findAll('.bench-player-card')
await playerCards[0].trigger('click')
await wrapper.vm.$nextTick()
const submitButton = wrapper.findAll('button').find(btn => btn.text().includes('Substitute Player'))
expect(submitButton?.attributes('disabled')).toBeUndefined()
})
it('disables submit button when playerOut is null', () => {
const wrapper = mount(PinchHitterSelector, {
props: {
...defaultProps,
playerOut: null,
},
})
const submitButton = wrapper.findAll('button').find(btn => btn.text().includes('Substitute Player'))
expect(submitButton?.attributes('disabled')).toBeDefined()
})
})
describe('Emit Events', () => {
it('emits submit event with correct payload', async () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
// Select a player
const playerCards = wrapper.findAll('.bench-player-card')
await playerCards[0].trigger('click')
// Click submit
const submitButton = wrapper.find('.button-submit')
await submitButton.trigger('click')
expect(wrapper.emitted()).toHaveProperty('submit')
const submitEvents = wrapper.emitted('submit')
expect(submitEvents).toHaveLength(1)
expect(submitEvents![0]).toEqual([{
playerOutLineupId: 1,
playerInCardId: 201,
teamId: 1,
}])
})
it('emits cancel event when cancel clicked', async () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
const cancelButton = wrapper.find('.button-cancel')
await cancelButton.trigger('click')
expect(wrapper.emitted()).toHaveProperty('cancel')
expect(wrapper.emitted('cancel')).toHaveLength(1)
})
it('resets selection after submit', async () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
const playerCards = wrapper.findAll('.bench-player-card')
await playerCards[0].trigger('click')
expect(wrapper.vm.selectedPlayerId).toBe(201)
const submitButton = wrapper.find('.button-submit')
await submitButton.trigger('click')
expect(wrapper.vm.selectedPlayerId).toBeNull()
})
it('resets selection after cancel', async () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
const playerCards = wrapper.findAll('.bench-player-card')
await playerCards[0].trigger('click')
expect(wrapper.vm.selectedPlayerId).toBe(201)
const cancelButton = wrapper.find('.button-cancel')
await cancelButton.trigger('click')
expect(wrapper.vm.selectedPlayerId).toBeNull()
})
})
describe('Player Metadata Display', () => {
it('displays player positions', () => {
const playerWithMultiPos = createMockPlayer(301, 'Multi Position')
playerWithMultiPos.pos_1 = '1B'
playerWithMultiPos.pos_2 = 'OF'
playerWithMultiPos.pos_3 = '3B'
const bench = [createMockLineup(5, playerWithMultiPos, false)]
const wrapper = mount(PinchHitterSelector, {
props: {
...defaultProps,
benchPlayers: bench,
},
})
expect(wrapper.text()).toContain('1B, OF, 3B')
})
it('displays WARA stat when available', () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
expect(wrapper.text()).toContain('WARA: 2.5')
})
it('limits position display to first 3 positions', () => {
const playerWithManyPos = createMockPlayer(401, 'Utility')
playerWithManyPos.pos_1 = '1B'
playerWithManyPos.pos_2 = '2B'
playerWithManyPos.pos_3 = '3B'
playerWithManyPos.pos_4 = 'SS'
const bench = [createMockLineup(6, playerWithManyPos, false)]
const wrapper = mount(PinchHitterSelector, {
props: {
...defaultProps,
benchPlayers: bench,
},
})
const text = wrapper.text()
expect(text).toContain('1B, 2B, 3B')
expect(text).not.toContain('SS')
})
})
describe('Edge Cases', () => {
it('handles missing playerOut gracefully', () => {
const wrapper = mount(PinchHitterSelector, {
props: {
...defaultProps,
playerOut: null,
},
})
expect(wrapper.text()).toContain('Select a player from the bench to bat in place of current batter')
})
it('prevents submit when playerOut is null', async () => {
const wrapper = mount(PinchHitterSelector, {
props: {
...defaultProps,
playerOut: null,
},
})
const playerCards = wrapper.findAll('.bench-player-card')
await playerCards[0].trigger('click')
const submitButton = wrapper.findAll('button').find(btn => btn.text().includes('Substitute Player'))
if (submitButton) {
await submitButton.trigger('click')
}
expect(wrapper.emitted('submit')).toBeUndefined()
})
it('prevents submit when no player selected even after clicking submit', async () => {
const wrapper = mount(PinchHitterSelector, {
props: defaultProps,
})
const submitButton = wrapper.findAll('button').find(btn => btn.text().includes('Substitute Player'))
if (submitButton) {
await submitButton.trigger('click')
}
expect(wrapper.emitted('submit')).toBeUndefined()
})
})
})