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>
481 lines
14 KiB
TypeScript
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()
|
|
})
|
|
})
|
|
})
|