strat-gameplay-webapp/frontend-sba/tests/unit/components/Gameplay/GameplayPanel.spec.ts
Cal Corum be31e2ccb4 CLAUDE: Complete in-game UI overhaul with player cards and outcome wizard
Features:
- PlayerCardModal: Tap any player to view full playing card image
- OutcomeWizard: Progressive 3-step outcome selection (On Base/Out/X-Check)
- GameBoard: Expandable view showing all 9 fielder positions
- Post-roll card display: Shows batter/pitcher card based on d6 roll
- CurrentSituation: Tappable player cards with modal integration

Bug fixes:
- Fix batter not advancing after play (state_manager recovery logic)
- Add dark mode support for buttons and panels (partial - iOS issue noted)

New files:
- PlayerCardModal.vue, OutcomeWizard.vue, BottomSheet.vue
- outcomeFlow.ts constants for outcome category mapping
- TEST_PLAN_UI_OVERHAUL.md with 23/24 tests passing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 15:23:38 -06:00

481 lines
14 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import GameplayPanel from '~/components/Gameplay/GameplayPanel.vue'
import type { RollData, PlayResult } from '~/types'
import DiceRoller from '~/components/Gameplay/DiceRoller.vue'
import OutcomeWizard from '~/components/Gameplay/OutcomeWizard.vue'
import PlayResultComponent from '~/components/Gameplay/PlayResult.vue'
describe('GameplayPanel', () => {
const createRollData = (): RollData => ({
roll_id: 'test-roll-123',
d6_one: 3,
d6_two_a: 2,
d6_two_b: 4,
d6_two_total: 6,
chaos_d20: 15,
resolution_d20: 8,
check_wild_pitch: false,
check_passed_ball: false,
timestamp: '2025-01-13T12:00:00Z',
})
const createPlayResult = (): PlayResult => ({
play_number: 1,
outcome: 'STRIKEOUT',
description: 'Mike Trout strikes out swinging',
outs_recorded: 1,
runs_scored: 0,
runners_advanced: [],
batter_result: null,
new_state: {},
is_hit: false,
is_out: true,
is_walk: false,
is_strikeout: true,
})
const defaultProps = {
gameId: 'game-123',
isMyTurn: false,
canRollDice: false,
pendingRoll: null,
lastPlayResult: null,
canSubmitOutcome: false,
}
beforeEach(() => {
vi.clearAllTimers()
})
// ============================================================================
// Rendering Tests
// ============================================================================
describe('Rendering', () => {
it('renders gameplay panel container', () => {
const wrapper = mount(GameplayPanel, {
props: defaultProps,
})
expect(wrapper.find('.gameplay-panel').exists()).toBe(true)
expect(wrapper.text()).toContain('Gameplay')
})
it('renders panel header with status', () => {
const wrapper = mount(GameplayPanel, {
props: defaultProps,
})
expect(wrapper.find('.panel-header').exists()).toBe(true)
expect(wrapper.find('.status-indicator').exists()).toBe(true)
expect(wrapper.find('.status-text').exists()).toBe(true)
})
})
// ============================================================================
// Workflow State: Idle Tests
// ============================================================================
describe('Workflow State: Idle', () => {
it('shows idle state when canRollDice is false', () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
canRollDice: false,
},
})
expect(wrapper.find('.state-idle').exists()).toBe(true)
expect(wrapper.text()).toContain('Waiting for strategic decisions')
})
it('displays idle status indicator', () => {
const wrapper = mount(GameplayPanel, {
props: defaultProps,
})
expect(wrapper.find('.status-idle').exists()).toBe(true)
expect(wrapper.find('.status-text').text()).toBe('Waiting')
})
})
// ============================================================================
// Workflow State: Ready to Roll Tests
// ============================================================================
describe('Workflow State: Ready to Roll', () => {
it('shows ready state when canRollDice is true and my turn', () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
canRollDice: true,
isMyTurn: true,
},
})
expect(wrapper.find('.state-ready').exists()).toBe(true)
expect(wrapper.text()).toContain('Your turn! Roll the dice')
})
it('shows waiting message when not my turn', () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
canRollDice: true,
isMyTurn: false,
},
})
expect(wrapper.text()).toContain('Waiting for opponent to roll dice')
})
it('renders DiceRoller component when my turn', () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
canRollDice: true,
isMyTurn: true,
},
})
expect(wrapper.findComponent(DiceRoller).exists()).toBe(true)
})
it('displays active status when ready and my turn', () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
canRollDice: true,
isMyTurn: true,
},
})
expect(wrapper.find('.status-active').exists()).toBe(true)
expect(wrapper.find('.status-text').text()).toBe('Your Turn')
})
it('displays opponent turn status when ready but not my turn', () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
canRollDice: true,
isMyTurn: false,
},
})
expect(wrapper.find('.status-text').text()).toBe('Opponent Turn')
})
})
// ============================================================================
// Workflow State: Rolled Tests
// ============================================================================
describe('Workflow State: Rolled', () => {
it('shows rolled state when pendingRoll exists', () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
pendingRoll: createRollData(),
canSubmitOutcome: true,
},
})
expect(wrapper.find('.state-rolled').exists()).toBe(true)
})
it('renders DiceRoller with roll results', () => {
const rollData = createRollData()
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
pendingRoll: rollData,
canSubmitOutcome: true,
},
})
const diceRoller = wrapper.findComponent(DiceRoller)
expect(diceRoller.exists()).toBe(true)
expect(diceRoller.props('pendingRoll')).toEqual(rollData)
expect(diceRoller.props('canRoll')).toBe(false)
})
it('renders OutcomeWizard component', () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
pendingRoll: createRollData(),
canSubmitOutcome: true,
},
})
expect(wrapper.findComponent(OutcomeWizard).exists()).toBe(true)
})
it('displays active status when outcome entry active', () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
pendingRoll: createRollData(),
canSubmitOutcome: true,
},
})
expect(wrapper.find('.status-active').exists()).toBe(true)
expect(wrapper.find('.status-text').text()).toBe('Enter Outcome')
})
})
// ============================================================================
// Workflow State: Result Tests
// ============================================================================
describe('Workflow State: Result', () => {
it('shows result state when lastPlayResult exists', () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
lastPlayResult: createPlayResult(),
},
})
expect(wrapper.find('.state-result').exists()).toBe(true)
})
it('renders PlayResult component', () => {
const playResult = createPlayResult()
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
lastPlayResult: playResult,
},
})
const playResultComponent = wrapper.findComponent(PlayResultComponent)
expect(playResultComponent.exists()).toBe(true)
expect(playResultComponent.props('result')).toEqual(playResult)
})
it('displays success status when result shown', () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
lastPlayResult: createPlayResult(),
},
})
expect(wrapper.find('.status-success').exists()).toBe(true)
expect(wrapper.find('.status-text').text()).toBe('Play Complete')
})
it('prioritizes result state over other states', () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
canRollDice: true,
pendingRoll: createRollData(),
lastPlayResult: createPlayResult(),
},
})
expect(wrapper.find('.state-result').exists()).toBe(true)
expect(wrapper.find('.state-rolled').exists()).toBe(false)
expect(wrapper.find('.state-ready').exists()).toBe(false)
})
})
// ============================================================================
// Event Emission Tests
// ============================================================================
describe('Event Emission', () => {
it('emits rollDice when DiceRoller emits roll', async () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
canRollDice: true,
isMyTurn: true,
},
})
const diceRoller = wrapper.findComponent(DiceRoller)
await diceRoller.vm.$emit('roll')
expect(wrapper.emitted('rollDice')).toBeTruthy()
expect(wrapper.emitted('rollDice')).toHaveLength(1)
})
it('emits submitOutcome when ManualOutcomeEntry submits', async () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
pendingRoll: createRollData(),
canSubmitOutcome: true,
},
})
const outcomeWizard = wrapper.findComponent(OutcomeWizard)
const payload = { outcome: 'STRIKEOUT' as const, hitLocation: undefined }
await outcomeWizard.vm.$emit('submit', payload)
expect(wrapper.emitted('submitOutcome')).toBeTruthy()
expect(wrapper.emitted('submitOutcome')?.[0]).toEqual([payload])
})
it('emits dismissResult when PlayResult emits dismiss', async () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
lastPlayResult: createPlayResult(),
},
})
const playResult = wrapper.findComponent(PlayResultComponent)
await playResult.vm.$emit('dismiss')
expect(wrapper.emitted('dismissResult')).toBeTruthy()
expect(wrapper.emitted('dismissResult')).toHaveLength(1)
})
})
// ============================================================================
// Workflow State Transitions Tests
// ============================================================================
describe('Workflow State Transitions', () => {
it('transitions from idle to ready when canRollDice becomes true', async () => {
const wrapper = mount(GameplayPanel, {
props: defaultProps,
})
expect(wrapper.find('.state-idle').exists()).toBe(true)
await wrapper.setProps({ canRollDice: true, isMyTurn: true })
expect(wrapper.find('.state-idle').exists()).toBe(false)
expect(wrapper.find('.state-ready').exists()).toBe(true)
})
it('transitions from ready to rolled when pendingRoll set', async () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
canRollDice: true,
isMyTurn: true,
},
})
expect(wrapper.find('.state-ready').exists()).toBe(true)
await wrapper.setProps({
pendingRoll: createRollData(),
canRollDice: false,
canSubmitOutcome: true,
})
expect(wrapper.find('.state-ready').exists()).toBe(false)
expect(wrapper.find('.state-rolled').exists()).toBe(true)
})
it('transitions from rolled to result when lastPlayResult set', async () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
pendingRoll: createRollData(),
canSubmitOutcome: true,
},
})
expect(wrapper.find('.state-rolled').exists()).toBe(true)
await wrapper.setProps({
lastPlayResult: createPlayResult(),
pendingRoll: null,
})
expect(wrapper.find('.state-rolled').exists()).toBe(false)
expect(wrapper.find('.state-result').exists()).toBe(true)
})
it('transitions from result to idle when result dismissed', async () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
lastPlayResult: createPlayResult(),
},
})
expect(wrapper.find('.state-result').exists()).toBe(true)
await wrapper.setProps({ lastPlayResult: null })
expect(wrapper.find('.state-result').exists()).toBe(false)
expect(wrapper.find('.state-idle').exists()).toBe(true)
})
})
// ============================================================================
// Edge Cases
// ============================================================================
describe('Edge Cases', () => {
it('handles multiple rapid state changes', async () => {
const wrapper = mount(GameplayPanel, {
props: defaultProps,
})
await wrapper.setProps({ canRollDice: true, isMyTurn: true })
await wrapper.setProps({ pendingRoll: createRollData(), canSubmitOutcome: true })
await wrapper.setProps({ lastPlayResult: createPlayResult() })
expect(wrapper.find('.state-result').exists()).toBe(true)
})
it('handles missing gameId gracefully', () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
gameId: '',
},
})
expect(wrapper.find('.gameplay-panel').exists()).toBe(true)
})
it('handles all props being null/false', () => {
const wrapper = mount(GameplayPanel, {
props: {
gameId: 'test',
isMyTurn: false,
canRollDice: false,
pendingRoll: null,
lastPlayResult: null,
canSubmitOutcome: false,
},
})
expect(wrapper.find('.state-idle').exists()).toBe(true)
})
it('clears error when rolling dice', async () => {
const wrapper = mount(GameplayPanel, {
props: {
...defaultProps,
canRollDice: true,
isMyTurn: true,
},
})
// Manually set error (would normally come from failed operation)
wrapper.vm.error = 'Test error'
await wrapper.vm.$nextTick()
const diceRoller = wrapper.findComponent(DiceRoller)
await diceRoller.vm.$emit('roll')
expect(wrapper.vm.error).toBeNull()
})
})
})