strat-gameplay-webapp/frontend-sba/tests/unit/components/Gameplay/PlayResult.spec.ts
Cal Corum eab61ad966 CLAUDE: Phases 3.5, F1-F5 Complete - Statistics & Frontend Components
This commit captures work from multiple sessions building the statistics
system and frontend component library.

Backend - Phase 3.5: Statistics System
- Box score statistics with materialized views
- Play stat calculator for real-time updates
- Stat view refresher service
- Alembic migration for materialized views
- Test coverage: 41 new tests (all passing)

Frontend - Phase F1: Foundation
- Composables: useGameState, useGameActions, useWebSocket
- Type definitions and interfaces
- Store setup with Pinia

Frontend - Phase F2: Game Display
- ScoreBoard, GameBoard, CurrentSituation, PlayByPlay components
- Demo page at /demo

Frontend - Phase F3: Decision Inputs
- DefensiveSetup, OffensiveApproach, StolenBaseInputs components
- DecisionPanel orchestration
- Demo page at /demo-decisions
- Test coverage: 213 tests passing

Frontend - Phase F4: Dice & Manual Outcome
- DiceRoller component
- ManualOutcomeEntry with validation
- PlayResult display
- GameplayPanel orchestration
- Demo page at /demo-gameplay
- Test coverage: 119 tests passing

Frontend - Phase F5: Substitutions
- PinchHitterSelector, DefensiveReplacementSelector, PitchingChangeSelector
- SubstitutionPanel with tab navigation
- Demo page at /demo-substitutions
- Test coverage: 114 tests passing

Documentation:
- PHASE_3_5_HANDOFF.md - Statistics system handoff
- PHASE_F2_COMPLETE.md - Game display completion
- Frontend phase planning docs
- NEXT_SESSION.md updated for Phase F6

Configuration:
- Package updates (Nuxt 4 fixes)
- Tailwind config enhancements
- Game store updates

Test Status:
- Backend: 731/731 passing (100%)
- Frontend: 446/446 passing (100%)
- Total: 1,177 tests passing

Next Phase: F6 - Integration (wire all components into game page)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 09:52:30 -06:00

469 lines
13 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import PlayResult from '~/components/Gameplay/PlayResult.vue'
import type { PlayResult as PlayResultType } from '~/types'
describe('PlayResult', () => {
const createPlayResult = (overrides = {}): PlayResultType => ({
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,
...overrides,
})
beforeEach(() => {
vi.clearAllTimers()
vi.useFakeTimers()
})
// ============================================================================
// Rendering Tests
// ============================================================================
describe('Rendering', () => {
it('renders when result is provided', () => {
const result = createPlayResult()
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.play-result').exists()).toBe(true)
expect(wrapper.text()).toContain('Play #1')
})
it('does not render when result is null', () => {
const wrapper = mount(PlayResult, {
props: { result: null },
})
expect(wrapper.find('.play-result').exists()).toBe(false)
})
it('displays play description', () => {
const result = createPlayResult({
description: 'Custom play description',
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.text()).toContain('Custom play description')
})
it('displays play number correctly', () => {
const result = createPlayResult({ play_number: 42 })
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.text()).toContain('Play #42')
})
})
// ============================================================================
// Result Type Styling Tests
// ============================================================================
describe('Result Type Styling', () => {
it('applies runs styling when runs scored', () => {
const result = createPlayResult({
runs_scored: 2,
is_out: false,
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.result-runs').exists()).toBe(true)
})
it('applies hit styling when is_hit is true', () => {
const result = createPlayResult({
is_hit: true,
is_out: false,
runs_scored: 0,
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.result-hit').exists()).toBe(true)
})
it('applies out styling when is_out is true', () => {
const result = createPlayResult({
is_out: true,
is_hit: false,
runs_scored: 0,
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.result-out').exists()).toBe(true)
})
it('applies default styling when no special type', () => {
const result = createPlayResult({
is_out: false,
is_hit: false,
runs_scored: 0,
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.result-default').exists()).toBe(true)
})
it('prioritizes runs over hit styling', () => {
const result = createPlayResult({
runs_scored: 1,
is_hit: true,
is_out: false,
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.result-runs').exists()).toBe(true)
expect(wrapper.find('.result-hit').exists()).toBe(false)
})
})
// ============================================================================
// Stats Display Tests
// ============================================================================
describe('Stats Display', () => {
it('displays outs recorded', () => {
const result = createPlayResult({ outs_recorded: 2 })
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.stat-outs').exists()).toBe(true)
expect(wrapper.find('.stat-outs').text()).toContain('2')
expect(wrapper.find('.stat-outs').text()).toContain('Outs')
})
it('uses singular "Out" for single out', () => {
const result = createPlayResult({ outs_recorded: 1 })
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.stat-outs').text()).toContain('Out')
expect(wrapper.find('.stat-outs').text()).not.toContain('Outs')
})
it('displays runs scored', () => {
const result = createPlayResult({
runs_scored: 3,
is_out: false,
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.stat-runs').exists()).toBe(true)
expect(wrapper.find('.stat-runs').text()).toContain('3')
expect(wrapper.find('.stat-runs').text()).toContain('Runs')
})
it('uses singular "Run" for single run', () => {
const result = createPlayResult({
runs_scored: 1,
is_out: false,
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.stat-runs').text()).toContain('Run')
expect(wrapper.find('.stat-runs').text()).not.toContain('Runs')
})
it('displays hit indicator when is_hit true', () => {
const result = createPlayResult({
is_hit: true,
is_out: false,
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.stat-hit').exists()).toBe(true)
expect(wrapper.find('.stat-hit').text()).toContain('Hit')
})
it('hides stats when values are zero', () => {
const result = createPlayResult({
outs_recorded: 0,
runs_scored: 0,
is_hit: false,
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.stat-outs').exists()).toBe(false)
expect(wrapper.find('.stat-runs').exists()).toBe(false)
expect(wrapper.find('.stat-hit').exists()).toBe(false)
})
})
// ============================================================================
// Runner Advancement Tests
// ============================================================================
describe('Runner Advancement', () => {
it('displays runner advancement section when runners advanced', () => {
const result = createPlayResult({
runners_advanced: [
{ from: 1, to: 3, out: false },
],
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.runner-advancement').exists()).toBe(true)
expect(wrapper.text()).toContain('Runner Movement:')
})
it('hides runner advancement when no runners advanced', () => {
const result = createPlayResult({
runners_advanced: [],
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.runner-advancement').exists()).toBe(false)
})
it('formats base labels correctly', () => {
const result = createPlayResult({
runners_advanced: [
{ from: 0, to: 1 }, // Batter to 1st
{ from: 1, to: 2 }, // 1st to 2nd
{ from: 2, to: 3 }, // 2nd to 3rd
{ from: 3, to: 4 }, // 3rd to Home
],
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.text()).toContain('Batter')
expect(wrapper.text()).toContain('1st')
expect(wrapper.text()).toContain('2nd')
expect(wrapper.text()).toContain('3rd')
expect(wrapper.text()).toContain('Home')
})
it('shows "Out" for runners who are out', () => {
const result = createPlayResult({
runners_advanced: [
{ from: 1, to: 2, out: true },
],
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.advancement-out').exists()).toBe(true)
expect(wrapper.text()).toContain('Out')
})
it('displays multiple runner advancements', () => {
const result = createPlayResult({
runners_advanced: [
{ from: 1, to: 3 },
{ from: 0, to: 1 },
],
})
const wrapper = mount(PlayResult, {
props: { result },
})
const items = wrapper.findAll('.advancement-item')
expect(items).toHaveLength(2)
})
})
// ============================================================================
// Dismiss Button Tests
// ============================================================================
describe('Dismiss Button', () => {
it('shows dismiss button when autoHide is false', () => {
const result = createPlayResult()
const wrapper = mount(PlayResult, {
props: {
result,
autoHide: false,
},
})
expect(wrapper.find('.dismiss-button').exists()).toBe(true)
})
it('hides dismiss button when autoHide is true', () => {
const result = createPlayResult()
const wrapper = mount(PlayResult, {
props: {
result,
autoHide: true,
},
})
expect(wrapper.find('.dismiss-button').exists()).toBe(false)
})
it('emits dismiss event when button clicked', async () => {
const result = createPlayResult()
const wrapper = mount(PlayResult, {
props: {
result,
autoHide: false,
},
})
await wrapper.find('.dismiss-button').trigger('click')
expect(wrapper.emitted('dismiss')).toBeTruthy()
expect(wrapper.emitted('dismiss')).toHaveLength(1)
})
})
// ============================================================================
// Auto-Hide Tests
// ============================================================================
describe('Auto-Hide', () => {
it('emits dismiss after 5 seconds when autoHide is true', async () => {
const result = createPlayResult()
const wrapper = mount(PlayResult, {
props: {
result,
autoHide: true,
},
})
expect(wrapper.emitted('dismiss')).toBeFalsy()
vi.advanceTimersByTime(5000)
await wrapper.vm.$nextTick()
expect(wrapper.emitted('dismiss')).toBeTruthy()
expect(wrapper.emitted('dismiss')).toHaveLength(1)
})
it('does not auto-dismiss when autoHide is false', async () => {
const result = createPlayResult()
const wrapper = mount(PlayResult, {
props: {
result,
autoHide: false,
},
})
vi.advanceTimersByTime(10000)
await wrapper.vm.$nextTick()
expect(wrapper.emitted('dismiss')).toBeFalsy()
})
it('does not auto-dismiss when result is null', async () => {
const wrapper = mount(PlayResult, {
props: {
result: null,
autoHide: true,
},
})
vi.advanceTimersByTime(5000)
await wrapper.vm.$nextTick()
expect(wrapper.emitted('dismiss')).toBeFalsy()
})
})
// ============================================================================
// Edge Cases
// ============================================================================
describe('Edge Cases', () => {
it('handles very long play descriptions', () => {
const result = createPlayResult({
description: 'A'.repeat(200),
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.result-description').text()).toHaveLength(200)
})
it('handles high play numbers', () => {
const result = createPlayResult({ play_number: 999 })
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.text()).toContain('Play #999')
})
it('handles multiple outs recorded', () => {
const result = createPlayResult({ outs_recorded: 3 })
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.stat-outs').text()).toContain('3')
})
it('handles grand slam scenario (4 runs)', () => {
const result = createPlayResult({
runs_scored: 4,
is_hit: true,
outs_recorded: 0,
})
const wrapper = mount(PlayResult, {
props: { result },
})
expect(wrapper.find('.stat-runs').text()).toContain('4')
expect(wrapper.find('.result-runs').exists()).toBe(true)
})
})
})