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() }) }) })