540 lines
17 KiB
TypeScript
540 lines
17 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import { mount } from '@vue/test-utils'
|
|
import DefensiveReplacementSelector from '~/components/Substitutions/DefensiveReplacementSelector.vue'
|
|
import type { Lineup, SbaPlayer } from '~/types'
|
|
|
|
// Helper to create mock player
|
|
function createMockPlayer(id: number, name: string, positions: string[]): SbaPlayer {
|
|
const player: SbaPlayer = {
|
|
id,
|
|
name,
|
|
image: `player-${id}.jpg`,
|
|
pos_1: null,
|
|
pos_2: null,
|
|
pos_3: null,
|
|
pos_4: null,
|
|
pos_5: null,
|
|
pos_6: null,
|
|
pos_7: null,
|
|
pos_8: null,
|
|
wara: 2.0,
|
|
team_id: 1,
|
|
team_name: 'Test Team',
|
|
season: '2024',
|
|
strat_code: null,
|
|
bbref_id: null,
|
|
injury_rating: null,
|
|
}
|
|
|
|
positions.forEach((pos, index) => {
|
|
const key = `pos_${index + 1}` as keyof SbaPlayer
|
|
;(player as any)[key] = pos
|
|
})
|
|
|
|
return player
|
|
}
|
|
|
|
// Helper to create mock lineup entry
|
|
function createMockLineup(id: number, player: SbaPlayer, isActive: boolean, position = 'OF', isFatigued = false): Lineup {
|
|
return {
|
|
lineup_id: id, // Changed from 'id' to 'lineup_id'
|
|
card_id: player.id, // Changed from 'player_id' to 'card_id'
|
|
game_id: 'test-game-123',
|
|
team_id: 1,
|
|
position,
|
|
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('DefensiveReplacementSelector', () => {
|
|
const mockPlayerOut = createMockLineup(
|
|
1,
|
|
createMockPlayer(101, 'John Fielder', ['OF', 'CF']),
|
|
true,
|
|
'CF'
|
|
)
|
|
|
|
const mockBenchPlayers = [
|
|
createMockLineup(2, createMockPlayer(201, 'Outfield Sub', ['OF', 'CF', 'RF']), false),
|
|
createMockLineup(3, createMockPlayer(202, 'Infield Sub', ['2B', '3B', 'SS']), false),
|
|
createMockLineup(4, createMockPlayer(203, 'Corner Sub', ['1B', '3B']), false),
|
|
]
|
|
|
|
const defaultProps = {
|
|
playerOut: mockPlayerOut,
|
|
benchPlayers: mockBenchPlayers,
|
|
teamId: 1,
|
|
}
|
|
|
|
describe('Rendering', () => {
|
|
it('renders component with header', () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Defensive Replacement')
|
|
})
|
|
|
|
it('displays current player information', () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Current Player:')
|
|
expect(wrapper.text()).toContain('John Fielder')
|
|
expect(wrapper.text()).toContain('CF')
|
|
})
|
|
|
|
it('shows descriptive text with player name', () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Select a player from the bench to replace John Fielder in the field')
|
|
})
|
|
|
|
it('renders all field positions', () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']
|
|
positions.forEach(pos => {
|
|
expect(wrapper.text()).toContain(pos)
|
|
})
|
|
})
|
|
|
|
it('renders action buttons', () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Cancel')
|
|
expect(wrapper.text()).toContain('Substitute Player')
|
|
})
|
|
})
|
|
|
|
describe('Position Selection', () => {
|
|
it('allows selecting a position', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
|
|
expect(wrapper.vm.selectedPosition).toBe('CF')
|
|
})
|
|
|
|
it('highlights selected position', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
expect(cfButton!.classes()).toContain('position-selected')
|
|
})
|
|
|
|
it('allows changing position selection', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
const lfButton = positionButtons.find(btn => btn.text() === 'LF')
|
|
|
|
await cfButton!.trigger('click')
|
|
expect(wrapper.vm.selectedPosition).toBe('CF')
|
|
|
|
await lfButton!.trigger('click')
|
|
expect(wrapper.vm.selectedPosition).toBe('LF')
|
|
})
|
|
|
|
it('resets player selection when position changes', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
|
|
// Select a player
|
|
const playerCards = wrapper.findAll('.bench-player-card')
|
|
if (playerCards.length > 0) {
|
|
await playerCards[0].trigger('click')
|
|
const firstPlayerId = wrapper.vm.selectedPlayerId
|
|
|
|
// Change position
|
|
const lfButton = positionButtons.find(btn => btn.text() === 'LF')
|
|
await lfButton!.trigger('click')
|
|
|
|
// Player selection should be reset
|
|
expect(wrapper.vm.selectedPlayerId).toBeNull()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('Player Filtering', () => {
|
|
it('shows no players when no position selected', () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Select a position first')
|
|
})
|
|
|
|
it('filters players by selected position', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
// Outfield Sub can play CF
|
|
expect(wrapper.text()).toContain('Outfield Sub')
|
|
// Infield Sub cannot play CF
|
|
expect(wrapper.text()).not.toContain('Infield Sub')
|
|
})
|
|
|
|
it('filters out active players', async () => {
|
|
const mixedPlayers = [
|
|
createMockLineup(2, createMockPlayer(201, 'Active OF', ['LF', 'CF']), true),
|
|
createMockLineup(3, createMockPlayer(202, 'Bench OF', ['LF', 'CF']), false),
|
|
]
|
|
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: {
|
|
...defaultProps,
|
|
benchPlayers: mixedPlayers,
|
|
},
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const lfButton = positionButtons.find(btn => btn.text() === 'LF')
|
|
await lfButton!.trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
expect(wrapper.text()).not.toContain('Active OF')
|
|
expect(wrapper.text()).toContain('Bench OF')
|
|
})
|
|
|
|
it('filters out fatigued players', async () => {
|
|
const mixedPlayers = [
|
|
createMockLineup(2, createMockPlayer(201, 'Fatigued OF', ['LF', 'CF']), false, 'LF', true),
|
|
createMockLineup(3, createMockPlayer(202, 'Fresh OF', ['LF', 'CF']), false),
|
|
]
|
|
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: {
|
|
...defaultProps,
|
|
benchPlayers: mixedPlayers,
|
|
},
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const lfButton = positionButtons.find(btn => btn.text() === 'LF')
|
|
await lfButton!.trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
expect(wrapper.text()).not.toContain('Fatigued OF')
|
|
expect(wrapper.text()).toContain('Fresh OF')
|
|
})
|
|
|
|
it('shows no eligible players message for position', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: {
|
|
...defaultProps,
|
|
benchPlayers: [
|
|
createMockLineup(2, createMockPlayer(201, 'Only Pitcher', ['P']), false),
|
|
],
|
|
},
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
expect(wrapper.text()).toContain('No players eligible for CF')
|
|
})
|
|
})
|
|
|
|
describe('Player Selection', () => {
|
|
it('allows selecting an eligible player', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
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(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
const playerCards = wrapper.findAll('.bench-player-card')
|
|
await playerCards[0].trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
expect(playerCards[0].classes()).toContain('bench-player-selected')
|
|
})
|
|
|
|
it('shows checkmark on selected player', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
const playerCards = wrapper.findAll('.bench-player-card')
|
|
await playerCards[0].trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
expect(playerCards[0].find('.selected-indicator').exists()).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Submit Validation', () => {
|
|
it('disables submit when no position selected', () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const submitButton = wrapper.findAll('button').find(btn => btn.text().includes('Substitute Player'))
|
|
expect(submitButton?.attributes('disabled')).toBeDefined()
|
|
})
|
|
|
|
it('disables submit when position selected but no player', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
const submitButton = wrapper.findAll('button').find(btn => btn.text().includes('Substitute Player'))
|
|
expect(submitButton?.attributes('disabled')).toBeDefined()
|
|
})
|
|
|
|
it('enables submit when both position and player selected', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
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 when playerOut is null', () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
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(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
const playerCards = wrapper.findAll('.bench-player-card')
|
|
await playerCards[0].trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
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,
|
|
newPosition: 'CF',
|
|
teamId: 1,
|
|
}])
|
|
})
|
|
|
|
it('emits cancel event when cancel clicked', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
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 selections after submit', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
|
|
const playerCards = wrapper.findAll('.bench-player-card')
|
|
await playerCards[0].trigger('click')
|
|
|
|
const submitButton = wrapper.find('.button-submit')
|
|
await submitButton.trigger('click')
|
|
|
|
expect(wrapper.vm.selectedPosition).toBeNull()
|
|
expect(wrapper.vm.selectedPlayerId).toBeNull()
|
|
})
|
|
|
|
it('resets selections after cancel', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
|
|
const playerCards = wrapper.findAll('.bench-player-card')
|
|
await playerCards[0].trigger('click')
|
|
|
|
const cancelButton = wrapper.find('.button-cancel')
|
|
await cancelButton.trigger('click')
|
|
|
|
expect(wrapper.vm.selectedPosition).toBeNull()
|
|
expect(wrapper.vm.selectedPlayerId).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('Player Metadata Display', () => {
|
|
it('displays player positions', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
expect(wrapper.text()).toContain('OF, CF, RF')
|
|
})
|
|
|
|
it('displays WARA stat when available', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
expect(wrapper.text()).toContain('WARA: 2.0')
|
|
})
|
|
|
|
it('shows position filter in bench label', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: defaultProps,
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.trigger('click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
expect(wrapper.text()).toContain('(eligible for CF)')
|
|
})
|
|
})
|
|
|
|
describe('Edge Cases', () => {
|
|
it('handles missing playerOut gracefully', () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: {
|
|
...defaultProps,
|
|
playerOut: null,
|
|
},
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Select a player from the bench to replace current player in the field')
|
|
})
|
|
|
|
it('prevents submit when any required field is missing', async () => {
|
|
const wrapper = mount(DefensiveReplacementSelector, {
|
|
props: {
|
|
...defaultProps,
|
|
playerOut: null,
|
|
},
|
|
})
|
|
|
|
const positionButtons = wrapper.findAll('.position-button')
|
|
const cfButton = positionButtons.find(btn => btn.text() === 'CF')
|
|
await cfButton!.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()
|
|
})
|
|
})
|
|
})
|