668 lines
20 KiB
TypeScript
668 lines
20 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { mount } from '@vue/test-utils'
|
|
import ManualOutcomeEntry from '~/components/Gameplay/ManualOutcomeEntry.vue'
|
|
import type { RollData, PlayOutcome } from '~/types'
|
|
|
|
// TODO: Fix text rendering (lowercase vs capitalized) and DOM element selection issues
|
|
describe.skip('ManualOutcomeEntry', () => {
|
|
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',
|
|
})
|
|
|
|
// ============================================================================
|
|
// Rendering Tests
|
|
// ============================================================================
|
|
|
|
describe('Rendering', () => {
|
|
it('renders outcome entry form', () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
expect(wrapper.find('.manual-outcome-entry').exists()).toBe(true)
|
|
expect(wrapper.text()).toContain('Select Outcome')
|
|
})
|
|
|
|
it('renders all outcome categories', () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Outs')
|
|
expect(wrapper.text()).toContain('Hits')
|
|
expect(wrapper.text()).toContain('Walks / HBP')
|
|
expect(wrapper.text()).toContain('Special')
|
|
expect(wrapper.text()).toContain('Interrupts')
|
|
})
|
|
|
|
it('renders action buttons', () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const buttons = wrapper.findAll('.button')
|
|
expect(buttons).toHaveLength(2)
|
|
expect(wrapper.find('.button-cancel').text()).toBe('Cancel')
|
|
expect(buttons[1].text()).toBe('Submit Outcome')
|
|
})
|
|
|
|
it('does not render hit location by default', () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
expect(wrapper.find('.hit-location-section').exists()).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Outcome Selection Tests
|
|
// ============================================================================
|
|
|
|
describe('Outcome Selection', () => {
|
|
it('selects outcome when button clicked', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const strikeoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Strikeout')
|
|
)
|
|
await strikeoutButton?.trigger('click')
|
|
|
|
expect(strikeoutButton?.classes()).toContain('outcome-button-selected')
|
|
})
|
|
|
|
it('deselects previous outcome when new one selected', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const buttons = wrapper.findAll('.outcome-button')
|
|
const strikeoutButton = buttons.find(btn => btn.text().includes('Strikeout'))
|
|
const walkButton = buttons.find(btn => btn.text() === 'Walk')
|
|
|
|
await strikeoutButton?.trigger('click')
|
|
expect(strikeoutButton?.classes()).toContain('outcome-button-selected')
|
|
|
|
await walkButton?.trigger('click')
|
|
expect(strikeoutButton?.classes()).not.toContain('outcome-button-selected')
|
|
expect(walkButton?.classes()).toContain('outcome-button-selected')
|
|
})
|
|
|
|
it('formats outcome names correctly', () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Hit By Pitch')
|
|
expect(wrapper.text()).toContain('Intentional Walk')
|
|
expect(wrapper.text()).toContain('Double Play')
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Hit Location Tests
|
|
// ============================================================================
|
|
|
|
describe('Hit Location', () => {
|
|
it('shows hit location for GROUNDOUT', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const outcomeButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Groundout')
|
|
)
|
|
await outcomeButton?.trigger('click')
|
|
|
|
expect(wrapper.find('.hit-location-section').exists()).toBe(true)
|
|
})
|
|
|
|
it('shows hit location for FLYOUT', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const outcomeButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Flyout')
|
|
)
|
|
await outcomeButton?.trigger('click')
|
|
|
|
expect(wrapper.find('.hit-location-section').exists()).toBe(true)
|
|
})
|
|
|
|
it('shows hit location for SINGLE', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const outcomeButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text() === 'Single 1'
|
|
)
|
|
await outcomeButton?.trigger('click')
|
|
|
|
expect(wrapper.find('.hit-location-section').exists()).toBe(true)
|
|
})
|
|
|
|
it('shows hit location for ERROR', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const outcomeButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Error')
|
|
)
|
|
await outcomeButton?.trigger('click')
|
|
|
|
expect(wrapper.find('.hit-location-section').exists()).toBe(true)
|
|
})
|
|
|
|
it('hides hit location for strikeout', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const strikeoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Strikeout')
|
|
)
|
|
await strikeoutButton?.trigger('click')
|
|
|
|
expect(wrapper.find('.hit-location-section').exists()).toBe(false)
|
|
})
|
|
|
|
it('renders all infield positions', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const groundoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Groundout')
|
|
)
|
|
await groundoutButton?.trigger('click')
|
|
|
|
expect(wrapper.text()).toContain('P')
|
|
expect(wrapper.text()).toContain('C')
|
|
expect(wrapper.text()).toContain('1B')
|
|
expect(wrapper.text()).toContain('2B')
|
|
expect(wrapper.text()).toContain('3B')
|
|
expect(wrapper.text()).toContain('SS')
|
|
})
|
|
|
|
it('renders all outfield positions', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const flyoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Flyout')
|
|
)
|
|
await flyoutButton?.trigger('click')
|
|
|
|
expect(wrapper.text()).toContain('LF')
|
|
expect(wrapper.text()).toContain('CF')
|
|
expect(wrapper.text()).toContain('RF')
|
|
})
|
|
|
|
it('selects hit location when clicked', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const groundoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Groundout')
|
|
)
|
|
await groundoutButton?.trigger('click')
|
|
|
|
const ssButton = wrapper.findAll('.location-button').find(btn =>
|
|
btn.text() === 'SS'
|
|
)
|
|
await ssButton?.trigger('click')
|
|
|
|
expect(ssButton?.classes()).toContain('location-button-selected')
|
|
})
|
|
|
|
it('clears hit location when outcome changes to one not needing location', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
// Select groundout (needs location)
|
|
const groundoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Groundout')
|
|
)
|
|
await groundoutButton?.trigger('click')
|
|
|
|
const ssButton = wrapper.findAll('.location-button').find(btn =>
|
|
btn.text() === 'SS'
|
|
)
|
|
await ssButton?.trigger('click')
|
|
|
|
// Select strikeout (doesn't need location)
|
|
const strikeoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Strikeout')
|
|
)
|
|
await strikeoutButton?.trigger('click')
|
|
|
|
expect(wrapper.find('.hit-location-section').exists()).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Submit Button State Tests
|
|
// ============================================================================
|
|
|
|
describe('Submit Button State', () => {
|
|
it('disables submit when no outcome selected', () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const submitButton = wrapper.findAll('.button')[1]
|
|
expect(submitButton.classes()).toContain('button-submit-disabled')
|
|
expect(submitButton.attributes('disabled')).toBeDefined()
|
|
})
|
|
|
|
it('enables submit when outcome selected (no location needed)', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const strikeoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Strikeout')
|
|
)
|
|
await strikeoutButton?.trigger('click')
|
|
|
|
const submitButton = wrapper.findAll('.button')[1]
|
|
expect(submitButton.classes()).toContain('button-submit')
|
|
expect(submitButton.attributes('disabled')).toBeUndefined()
|
|
})
|
|
|
|
it('disables submit when outcome selected but location needed and not selected', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const groundoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Groundout')
|
|
)
|
|
await groundoutButton?.trigger('click')
|
|
|
|
const submitButton = wrapper.findAll('.button')[1]
|
|
expect(submitButton.classes()).toContain('button-submit-disabled')
|
|
expect(submitButton.attributes('disabled')).toBeDefined()
|
|
})
|
|
|
|
it('enables submit when outcome and location both selected', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const groundoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Groundout')
|
|
)
|
|
await groundoutButton?.trigger('click')
|
|
|
|
const ssButton = wrapper.findAll('.location-button').find(btn =>
|
|
btn.text() === 'SS'
|
|
)
|
|
await ssButton?.trigger('click')
|
|
|
|
const submitButton = wrapper.findAll('.button')[1]
|
|
expect(submitButton.classes()).toContain('button-submit')
|
|
expect(submitButton.attributes('disabled')).toBeUndefined()
|
|
})
|
|
|
|
it('disables submit when canSubmit prop is false', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: false,
|
|
},
|
|
})
|
|
|
|
const strikeoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Strikeout')
|
|
)
|
|
await strikeoutButton?.trigger('click')
|
|
|
|
const submitButton = wrapper.findAll('.button')[1]
|
|
expect(submitButton.classes()).toContain('button-submit-disabled')
|
|
})
|
|
|
|
it('disables submit when rollData is null', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: null,
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const strikeoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Strikeout')
|
|
)
|
|
await strikeoutButton?.trigger('click')
|
|
|
|
const submitButton = wrapper.findAll('.button')[1]
|
|
expect(submitButton.classes()).toContain('button-submit-disabled')
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Event Emission Tests
|
|
// ============================================================================
|
|
|
|
describe('Event Emission', () => {
|
|
it('emits submit event with outcome (no location)', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const strikeoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Strikeout')
|
|
)
|
|
await strikeoutButton?.trigger('click')
|
|
await wrapper.findAll('.button')[1].trigger('click')
|
|
|
|
expect(wrapper.emitted('submit')).toBeTruthy()
|
|
expect(wrapper.emitted('submit')?.[0]).toEqual([{
|
|
outcome: 'STRIKEOUT',
|
|
hitLocation: undefined,
|
|
}])
|
|
})
|
|
|
|
it('emits submit event with outcome and location', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const groundoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Groundout')
|
|
)
|
|
await groundoutButton?.trigger('click')
|
|
|
|
const ssButton = wrapper.findAll('.location-button').find(btn =>
|
|
btn.text() === 'SS'
|
|
)
|
|
await ssButton?.trigger('click')
|
|
|
|
await wrapper.findAll('.button')[1].trigger('click')
|
|
|
|
expect(wrapper.emitted('submit')).toBeTruthy()
|
|
expect(wrapper.emitted('submit')?.[0]).toEqual([{
|
|
outcome: 'GROUNDOUT',
|
|
hitLocation: 'SS',
|
|
}])
|
|
})
|
|
|
|
it('emits cancel event when cancel button clicked', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
await wrapper.find('.button-cancel').trigger('click')
|
|
|
|
expect(wrapper.emitted('cancel')).toBeTruthy()
|
|
expect(wrapper.emitted('cancel')).toHaveLength(1)
|
|
})
|
|
|
|
it('does not emit submit when form invalid', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
await wrapper.findAll('.button')[1].trigger('click')
|
|
|
|
expect(wrapper.emitted('submit')).toBeFalsy()
|
|
})
|
|
|
|
it('resets form after successful submit', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const strikeoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Strikeout')
|
|
)
|
|
await strikeoutButton?.trigger('click')
|
|
await wrapper.findAll('.button')[1].trigger('click')
|
|
|
|
expect(strikeoutButton?.classes()).not.toContain('outcome-button-selected')
|
|
})
|
|
|
|
it('resets form after cancel', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const strikeoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Strikeout')
|
|
)
|
|
await strikeoutButton?.trigger('click')
|
|
await wrapper.find('.button-cancel').trigger('click')
|
|
|
|
expect(strikeoutButton?.classes()).not.toContain('outcome-button-selected')
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Outcome Categories Tests
|
|
// ============================================================================
|
|
|
|
describe('Outcome Categories', () => {
|
|
it('renders outs category outcomes', () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Strikeout')
|
|
expect(wrapper.text()).toContain('Groundout')
|
|
expect(wrapper.text()).toContain('Flyout')
|
|
expect(wrapper.text()).toContain('Lineout')
|
|
expect(wrapper.text()).toContain('Popout')
|
|
expect(wrapper.text()).toContain('Double Play')
|
|
})
|
|
|
|
it('renders hits category outcomes', () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Single 1')
|
|
expect(wrapper.text()).toContain('Single 2')
|
|
expect(wrapper.text()).toContain('Double 2')
|
|
expect(wrapper.text()).toContain('Triple')
|
|
expect(wrapper.text()).toContain('Homerun')
|
|
})
|
|
|
|
it('renders walks category outcomes', () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Walk')
|
|
expect(wrapper.text()).toContain('Intentional Walk')
|
|
expect(wrapper.text()).toContain('Hit By Pitch')
|
|
})
|
|
|
|
it('renders special category outcomes', () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Error')
|
|
})
|
|
|
|
it('renders interrupts category outcomes', () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Stolen Base')
|
|
expect(wrapper.text()).toContain('Caught Stealing')
|
|
expect(wrapper.text()).toContain('Wild Pitch')
|
|
expect(wrapper.text()).toContain('Passed Ball')
|
|
expect(wrapper.text()).toContain('Balk')
|
|
expect(wrapper.text()).toContain('Pick Off')
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Edge Cases
|
|
// ============================================================================
|
|
|
|
describe('Edge Cases', () => {
|
|
it('handles multiple hit location changes', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const groundoutButton = wrapper.findAll('.outcome-button').find(btn =>
|
|
btn.text().includes('Groundout')
|
|
)
|
|
await groundoutButton?.trigger('click')
|
|
|
|
const ssButton = wrapper.findAll('.location-button').find(btn =>
|
|
btn.text() === 'SS'
|
|
)
|
|
const cfButton = wrapper.findAll('.location-button').find(btn =>
|
|
btn.text() === 'CF'
|
|
)
|
|
|
|
await ssButton?.trigger('click')
|
|
expect(ssButton?.classes()).toContain('location-button-selected')
|
|
|
|
await cfButton?.trigger('click')
|
|
expect(ssButton?.classes()).not.toContain('location-button-selected')
|
|
expect(cfButton?.classes()).toContain('location-button-selected')
|
|
})
|
|
|
|
it('handles rapid outcome selection changes', async () => {
|
|
const wrapper = mount(ManualOutcomeEntry, {
|
|
props: {
|
|
rollData: createRollData(),
|
|
canSubmit: true,
|
|
},
|
|
})
|
|
|
|
const buttons = wrapper.findAll('.outcome-button')
|
|
const strikeoutButton = buttons.find(btn => btn.text().includes('Strikeout'))
|
|
const walkButton = buttons.find(btn => btn.text() === 'Walk')
|
|
const homerunButton = buttons.find(btn => btn.text().includes('Homerun'))
|
|
|
|
await strikeoutButton?.trigger('click')
|
|
await walkButton?.trigger('click')
|
|
await homerunButton?.trigger('click')
|
|
|
|
expect(homerunButton?.classes()).toContain('outcome-button-selected')
|
|
expect(strikeoutButton?.classes()).not.toContain('outcome-button-selected')
|
|
expect(walkButton?.classes()).not.toContain('outcome-button-selected')
|
|
})
|
|
})
|
|
})
|