import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import SubstitutionPanel from '~/components/Substitutions/SubstitutionPanel.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', battingOrder: number | null = null ): Lineup { return { id, game_id: 'test-game-123', team_id: 1, player_id: player.id, position, batting_order: battingOrder, is_starter: true, is_active: isActive, entered_inning: 1, replacing_id: null, after_play: null, is_fatigued: false, player, } } describe('SubstitutionPanel', () => { const mockCurrentLineup = [ createMockLineup(1, createMockPlayer(101, 'John Pitcher', ['P']), true, 'P', null), createMockLineup(2, createMockPlayer(102, 'Jane Catcher', ['C']), true, 'C', 1), createMockLineup(3, createMockPlayer(103, 'Bob First', ['1B']), true, '1B', 2), createMockLineup(4, createMockPlayer(104, 'Sally Second', ['2B']), true, '2B', 3), ] const mockBenchPlayers = [ createMockLineup(5, createMockPlayer(201, 'Relief Pitcher', ['P']), false), createMockLineup(6, createMockPlayer(202, 'Bench Hitter', ['OF']), false), createMockLineup(7, createMockPlayer(203, 'Utility Player', ['2B', '3B', 'SS']), false), ] const defaultProps = { gameId: 'test-game-123', teamId: 1, currentLineup: mockCurrentLineup, benchPlayers: mockBenchPlayers, currentPitcher: mockCurrentLineup[0], currentBatter: mockCurrentLineup[1], } describe('Rendering', () => { it('renders component with header', () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) expect(wrapper.text()).toContain('Substitutions') }) it('renders all three tab buttons', () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) expect(wrapper.text()).toContain('Pinch Hitter') expect(wrapper.text()).toContain('Defensive Sub') expect(wrapper.text()).toContain('Pitching Change') }) it('shows idle state by default', () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) expect(wrapper.text()).toContain('Select a substitution type above') }) it('displays status indicator', () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const statusIndicator = wrapper.find('.status-indicator') expect(statusIndicator.exists()).toBe(true) }) }) describe('Tab Navigation', () => { it('activates pinch hitter tab when clicked', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const tabs = wrapper.findAll('.tab-button') const pinchHitterTab = tabs.find(tab => tab.text().includes('Pinch Hitter')) await pinchHitterTab!.trigger('click') expect(wrapper.vm.currentTab).toBe('pinch_hitter') }) it('activates defensive replacement tab when clicked', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const tabs = wrapper.findAll('.tab-button') const defensiveTab = tabs.find(tab => tab.text().includes('Defensive Sub')) await defensiveTab!.trigger('click') expect(wrapper.vm.currentTab).toBe('defensive_replacement') }) it('activates pitching change tab when clicked', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const tabs = wrapper.findAll('.tab-button') const pitchingTab = tabs.find(tab => tab.text().includes('Pitching Change')) await pitchingTab!.trigger('click') expect(wrapper.vm.currentTab).toBe('pitching_change') }) it('highlights active tab', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const tabs = wrapper.findAll('.tab-button') const pinchHitterTab = tabs.find(tab => tab.text().includes('Pinch Hitter')) await pinchHitterTab!.trigger('click') await wrapper.vm.$nextTick() expect(pinchHitterTab!.classes()).toContain('tab-active') }) it('allows switching between tabs', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const tabs = wrapper.findAll('.tab-button') const pinchHitterTab = tabs.find(tab => tab.text().includes('Pinch Hitter')) const pitchingTab = tabs.find(tab => tab.text().includes('Pitching Change')) await pinchHitterTab!.trigger('click') expect(wrapper.vm.currentTab).toBe('pinch_hitter') await pitchingTab!.trigger('click') expect(wrapper.vm.currentTab).toBe('pitching_change') }) }) describe('Pinch Hitter Mode', () => { it('renders PinchHitterSelector when tab selected', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const tabs = wrapper.findAll('.tab-button') const pinchHitterTab = tabs.find(tab => tab.text().includes('Pinch Hitter')) await pinchHitterTab!.trigger('click') await wrapper.vm.$nextTick() expect(wrapper.text()).toContain('Select a bench player to bat in place of the current batter') }) it('shows instruction box for pinch hitter', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const tabs = wrapper.findAll('.tab-button') const pinchHitterTab = tabs.find(tab => tab.text().includes('Pinch Hitter')) await pinchHitterTab!.trigger('click') await wrapper.vm.$nextTick() const instructionBox = wrapper.find('.instruction-box') expect(instructionBox.exists()).toBe(true) }) }) describe('Defensive Replacement Mode', () => { it('renders DefensiveReplacementSelector when tab selected', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const tabs = wrapper.findAll('.tab-button') const defensiveTab = tabs.find(tab => tab.text().includes('Defensive Sub')) await defensiveTab!.trigger('click') await wrapper.vm.$nextTick() expect(wrapper.text()).toContain('Replace a defensive player with a bench player') }) it('shows active fielders for selection', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const tabs = wrapper.findAll('.tab-button') const defensiveTab = tabs.find(tab => tab.text().includes('Defensive Sub')) await defensiveTab!.trigger('click') await wrapper.vm.$nextTick() expect(wrapper.text()).toContain('Select player to replace:') expect(wrapper.text()).toContain('Jane Catcher') expect(wrapper.text()).toContain('Bob First') }) it('excludes pitcher from defensive replacement selection', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const tabs = wrapper.findAll('.tab-button') const defensiveTab = tabs.find(tab => tab.text().includes('Defensive Sub')) await defensiveTab!.trigger('click') await wrapper.vm.$nextTick() expect(wrapper.text()).not.toContain('John Pitcher') }) }) describe('Pitching Change Mode', () => { it('renders PitchingChangeSelector when tab selected', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const tabs = wrapper.findAll('.tab-button') const pitchingTab = tabs.find(tab => tab.text().includes('Pitching Change')) await pitchingTab!.trigger('click') await wrapper.vm.$nextTick() expect(wrapper.text()).toContain('Bring in a relief pitcher from the bench') }) }) describe('Event Emissions', () => { it('emits pinchHitter event with correct payload', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const tabs = wrapper.findAll('.tab-button') const pinchHitterTab = tabs.find(tab => tab.text().includes('Pinch Hitter')) await pinchHitterTab!.trigger('click') const testPayload = { playerOutLineupId: 1, playerInCardId: 201, teamId: 1, } // Manually trigger the handler wrapper.vm.handlePinchHitterSubmit(testPayload) await wrapper.vm.$nextTick() expect(wrapper.emitted()).toHaveProperty('pinchHitter') expect(wrapper.emitted('pinchHitter')![0]).toEqual([testPayload]) }) it('emits defensiveReplacement event with correct payload', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const testPayload = { playerOutLineupId: 2, playerInCardId: 203, newPosition: '2B', teamId: 1, } wrapper.vm.handleDefensiveReplacementSubmit(testPayload) await wrapper.vm.$nextTick() expect(wrapper.emitted()).toHaveProperty('defensiveReplacement') expect(wrapper.emitted('defensiveReplacement')![0]).toEqual([testPayload]) }) it('emits pitchingChange event with correct payload', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const testPayload = { playerOutLineupId: 1, playerInCardId: 201, teamId: 1, } wrapper.vm.handlePitchingChangeSubmit(testPayload) await wrapper.vm.$nextTick() expect(wrapper.emitted()).toHaveProperty('pitchingChange') expect(wrapper.emitted('pitchingChange')![0]).toEqual([testPayload]) }) it('emits cancel event when selector cancelled', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) wrapper.vm.handleCancel() await wrapper.vm.$nextTick() expect(wrapper.emitted()).toHaveProperty('cancel') }) }) describe('Status Management', () => { it('shows active status when tab selected', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const tabs = wrapper.findAll('.tab-button') const pinchHitterTab = tabs.find(tab => tab.text().includes('Pinch Hitter')) await pinchHitterTab!.trigger('click') await wrapper.vm.$nextTick() expect(wrapper.vm.statusClass).toBe('status-active') expect(wrapper.vm.statusText).toBe('Active') }) it('shows success status after successful submission', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const testPayload = { playerOutLineupId: 1, playerInCardId: 201, teamId: 1, } wrapper.vm.handlePinchHitterSubmit(testPayload) await wrapper.vm.$nextTick() expect(wrapper.vm.statusClass).toBe('status-success') expect(wrapper.vm.statusText).toBe('Success') }) it('shows idle status by default', () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) expect(wrapper.vm.statusClass).toBe('status-idle') expect(wrapper.vm.statusText).toBe('Idle') }) }) describe('Success Messages', () => { it('displays success message after pinch hitter submission', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const testPayload = { playerOutLineupId: 1, playerInCardId: 201, teamId: 1, } wrapper.vm.handlePinchHitterSubmit(testPayload) await wrapper.vm.$nextTick() expect(wrapper.text()).toContain('Pinch hitter substitution submitted') }) it('displays success message after defensive replacement', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const testPayload = { playerOutLineupId: 2, playerInCardId: 203, newPosition: '2B', teamId: 1, } wrapper.vm.handleDefensiveReplacementSubmit(testPayload) await wrapper.vm.$nextTick() expect(wrapper.text()).toContain('Defensive replacement submitted') }) it('displays success message after pitching change', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const testPayload = { playerOutLineupId: 1, playerInCardId: 201, teamId: 1, } wrapper.vm.handlePitchingChangeSubmit(testPayload) await wrapper.vm.$nextTick() expect(wrapper.text()).toContain('Pitching change submitted') }) }) describe('State Reset', () => { it('resets to idle after cancel', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) const tabs = wrapper.findAll('.tab-button') const pinchHitterTab = tabs.find(tab => tab.text().includes('Pinch Hitter')) await pinchHitterTab!.trigger('click') expect(wrapper.vm.currentTab).toBe('pinch_hitter') wrapper.vm.handleCancel() await wrapper.vm.$nextTick() expect(wrapper.vm.currentTab).toBeNull() }) it('clears error when changing tabs', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) wrapper.vm.error = 'Test error' await wrapper.vm.$nextTick() const tabs = wrapper.findAll('.tab-button') const pinchHitterTab = tabs.find(tab => tab.text().includes('Pinch Hitter')) await pinchHitterTab!.trigger('click') expect(wrapper.vm.error).toBeNull() }) it('clears defensive player selection when switching away from defensive tab', async () => { const wrapper = mount(SubstitutionPanel, { props: defaultProps, }) wrapper.vm.selectedDefensivePlayer = mockCurrentLineup[1] await wrapper.vm.$nextTick() const tabs = wrapper.findAll('.tab-button') const pinchHitterTab = tabs.find(tab => tab.text().includes('Pinch Hitter')) await pinchHitterTab!.trigger('click') expect(wrapper.vm.selectedDefensivePlayer).toBeNull() }) }) describe('Edge Cases', () => { it('handles empty bench players gracefully', () => { const wrapper = mount(SubstitutionPanel, { props: { ...defaultProps, benchPlayers: [], }, }) expect(wrapper.exists()).toBe(true) }) it('handles missing currentPitcher', () => { const wrapper = mount(SubstitutionPanel, { props: { ...defaultProps, currentPitcher: null, }, }) expect(wrapper.exists()).toBe(true) }) it('handles missing currentBatter', () => { const wrapper = mount(SubstitutionPanel, { props: { ...defaultProps, currentBatter: null, }, }) expect(wrapper.exists()).toBe(true) }) }) })