import { describe, it, expect, beforeEach, vi } from "vitest"; import { mount } from "@vue/test-utils"; import { createPinia, setActivePinia } from "pinia"; import RunnersOnBase from "~/components/Game/RunnersOnBase.vue"; import RunnerCard from "~/components/Game/RunnerCard.vue"; import { useGameStore } from "~/store/game"; import type { LineupPlayerState, Lineup } from "~/types/game"; describe("RunnersOnBase", () => { let pinia: ReturnType; beforeEach(() => { pinia = createPinia(); setActivePinia(pinia); }); const mockRunnerFirst: LineupPlayerState = { lineup_id: 1, batting_order: 1, position: "LF", card_id: 101, }; const mockRunnerSecond: LineupPlayerState = { lineup_id: 2, batting_order: 2, position: "CF", card_id: 102, }; const mockRunnerThird: LineupPlayerState = { lineup_id: 3, batting_order: 3, position: "RF", card_id: 103, }; const mockCatcher: Lineup = { id: 1, lineup_id: 4, team_id: 1, batting_order: 4, position: "C", is_active: true, player: { id: 104, name: "Buster Posey", image: "https://example.com/posey.jpg", headshot: "https://example.com/posey-headshot.jpg", }, }; const mockFieldingLineup: Lineup[] = [mockCatcher]; describe("component visibility", () => { it("does not render when no runners on base", () => { const wrapper = mount(RunnersOnBase, { global: { plugins: [pinia] }, props: { runners: { first: null, second: null, third: null }, fieldingLineup: mockFieldingLineup, battingTeamColor: "#3b82f6", fieldingTeamColor: "#10b981", battingTeamAbbrev: "BOS", fieldingTeamAbbrev: "NYY", }, }); expect(wrapper.find(".runners-on-base-container").exists()).toBe( false, ); }); it("renders when at least one runner on base", () => { const wrapper = mount(RunnersOnBase, { global: { plugins: [pinia] }, props: { runners: { first: null, second: mockRunnerSecond, third: null, }, fieldingLineup: mockFieldingLineup, battingTeamColor: "#3b82f6", fieldingTeamColor: "#10b981", battingTeamAbbrev: "BOS", fieldingTeamAbbrev: "NYY", }, }); expect(wrapper.find(".runners-on-base-container").exists()).toBe( true, ); }); it("renders when bases loaded", () => { const wrapper = mount(RunnersOnBase, { global: { plugins: [pinia] }, props: { runners: { first: mockRunnerFirst, second: mockRunnerSecond, third: mockRunnerThird, }, fieldingLineup: mockFieldingLineup, battingTeamColor: "#3b82f6", fieldingTeamColor: "#10b981", battingTeamAbbrev: "BOS", fieldingTeamAbbrev: "NYY", }, }); expect(wrapper.find(".runners-on-base-container").exists()).toBe( true, ); }); }); describe("runner cards", () => { it("renders three RunnerCard components", () => { const wrapper = mount(RunnersOnBase, { global: { plugins: [pinia] }, props: { runners: { first: mockRunnerFirst, second: null, third: null, }, fieldingLineup: mockFieldingLineup, battingTeamColor: "#3b82f6", fieldingTeamColor: "#10b981", battingTeamAbbrev: "BOS", fieldingTeamAbbrev: "NYY", }, }); const runnerCards = wrapper.findAllComponents(RunnerCard); expect(runnerCards).toHaveLength(3); }); it("passes correct base labels to RunnerCard components", async () => { const wrapper = mount(RunnersOnBase, { global: { plugins: [pinia] }, props: { runners: { first: mockRunnerFirst, second: null, third: null, }, fieldingLineup: mockFieldingLineup, battingTeamColor: "#3b82f6", fieldingTeamColor: "#10b981", battingTeamAbbrev: "BOS", fieldingTeamAbbrev: "NYY", }, }); await wrapper.vm.$nextTick(); const runnerCards = wrapper.findAllComponents(RunnerCard); expect(runnerCards.length).toBeGreaterThanOrEqual(3); // Order is now 3B, 2B, 1B (left to right) expect(runnerCards[0].props("base")).toBe("3B"); expect(runnerCards[1].props("base")).toBe("2B"); expect(runnerCards[2].props("base")).toBe("1B"); }); it("passes runner data to RunnerCard components", () => { const wrapper = mount(RunnersOnBase, { global: { plugins: [pinia] }, props: { runners: { first: mockRunnerFirst, second: mockRunnerSecond, third: null, }, fieldingLineup: mockFieldingLineup, battingTeamColor: "#3b82f6", fieldingTeamColor: "#10b981", battingTeamAbbrev: "BOS", fieldingTeamAbbrev: "NYY", }, }); const runnerCards = wrapper.findAllComponents(RunnerCard); // Order is now 3B, 2B, 1B (left to right) expect(runnerCards[0].props("runner")).toBeNull(); // 3B expect(runnerCards[1].props("runner")).toEqual(mockRunnerSecond); // 2B expect(runnerCards[2].props("runner")).toEqual(mockRunnerFirst); // 1B }); it("passes team color to RunnerCard components", () => { const wrapper = mount(RunnersOnBase, { global: { plugins: [pinia] }, props: { runners: { first: mockRunnerFirst, second: null, third: null, }, fieldingLineup: mockFieldingLineup, battingTeamColor: "#ff0000", fieldingTeamColor: "#00ff00", battingTeamAbbrev: "BOS", fieldingTeamAbbrev: "NYY", }, }); const runnerCards = wrapper.findAllComponents(RunnerCard); // Runner cards now use hardcoded red (#ef4444) instead of battingTeamColor runnerCards.forEach((card) => { expect(card.props("teamColor")).toBe("#ef4444"); }); }); }); describe("catcher display", () => { it("shows catcher summary pill by default", () => { const wrapper = mount(RunnersOnBase, { global: { plugins: [pinia] }, props: { runners: { first: mockRunnerFirst, second: null, third: null, }, fieldingLineup: mockFieldingLineup, battingTeamColor: "#3b82f6", fieldingTeamColor: "#10b981", battingTeamAbbrev: "BOS", fieldingTeamAbbrev: "NYY", }, }); // Catcher summary pill is always visible expect(wrapper.find(".catcher-pill").exists()).toBe(true); // Expanded view not shown by default expect(wrapper.findAll(".matchup-card")).toHaveLength(0); }); it("displays catcher name", () => { const wrapper = mount(RunnersOnBase, { global: { plugins: [pinia] }, props: { runners: { first: mockRunnerFirst, second: null, third: null, }, fieldingLineup: mockFieldingLineup, battingTeamColor: "#3b82f6", fieldingTeamColor: "#10b981", battingTeamAbbrev: "BOS", fieldingTeamAbbrev: "NYY", }, }); expect(wrapper.text()).toContain("Buster Posey"); }); it('shows "Unknown Catcher" when no catcher in lineup', () => { const wrapper = mount(RunnersOnBase, { global: { plugins: [pinia] }, props: { runners: { first: mockRunnerFirst, second: null, third: null, }, fieldingLineup: [], battingTeamColor: "#3b82f6", fieldingTeamColor: "#10b981", battingTeamAbbrev: "BOS", fieldingTeamAbbrev: "NYY", }, }); expect(wrapper.text()).toContain("Unknown Catcher"); }); }); describe("runner selection", () => { it("shows expanded detail row when runner is selected", async () => { const gameStore = useGameStore(); gameStore.setGameState({ id: 1, home_team_id: 1, away_team_id: 2, status: "active", inning: 1, half: "top", outs: 0, home_score: 0, away_score: 0, home_team_abbrev: "NYY", away_team_abbrev: "BOS", home_team_dice_color: "3b82f6", current_batter: null, current_pitcher: null, on_first: null, on_second: null, on_third: null, decision_phase: "idle", play_count: 0, }); gameStore.updateLineup(1, [ { id: 1, lineup_id: 1, team_id: 1, batting_order: 1, position: "LF", is_active: true, player: { id: 101, name: "Mike Trout", image: "https://example.com/trout.jpg", }, }, ]); const wrapper = mount(RunnersOnBase, { global: { plugins: [pinia] }, props: { runners: { first: mockRunnerFirst, second: null, third: null, }, fieldingLineup: mockFieldingLineup, battingTeamColor: "#3b82f6", fieldingTeamColor: "#10b981", battingTeamAbbrev: "BOS", fieldingTeamAbbrev: "NYY", }, }); const runnerCards = wrapper.findAllComponents(RunnerCard); await runnerCards[0].trigger("click"); // When runner selected, expanded detail row shows both runner + catcher cards // matchup-card-red is runner (red), matchup-card-blue is catcher (blue) expect(wrapper.find(".matchup-card-red").exists()).toBe(true); // Runner full card expect(wrapper.find(".matchup-card-blue").exists()).toBe(true); // Catcher full card }); it("hides expanded detail row when runner is deselected", async () => { const gameStore = useGameStore(); gameStore.setGameState({ id: 1, home_team_id: 1, away_team_id: 2, status: "active", inning: 1, half: "top", outs: 0, home_score: 0, away_score: 0, home_team_abbrev: "NYY", away_team_abbrev: "BOS", home_team_dice_color: "3b82f6", current_batter: null, current_pitcher: null, on_first: null, on_second: null, on_third: null, decision_phase: "idle", play_count: 0, }); gameStore.updateLineup(1, [ { id: 1, lineup_id: 1, team_id: 1, batting_order: 1, position: "LF", is_active: true, player: { id: 101, name: "Mike Trout", image: "https://example.com/trout.jpg", }, }, ]); const wrapper = mount(RunnersOnBase, { global: { plugins: [pinia] }, props: { runners: { first: mockRunnerFirst, second: null, third: null, }, fieldingLineup: mockFieldingLineup, battingTeamColor: "#3b82f6", fieldingTeamColor: "#10b981", battingTeamAbbrev: "BOS", fieldingTeamAbbrev: "NYY", }, }); const runnerCards = wrapper.findAllComponents(RunnerCard); // Click to show expanded view await runnerCards[0].trigger("click"); await wrapper.vm.$nextTick(); expect(wrapper.find(".matchup-card-red").exists()).toBe(true); // Runner (red) expect(wrapper.find(".matchup-card-blue").exists()).toBe(true); // Catcher (blue) // Click again to toggle selection off - Transition may keep elements during animation await runnerCards[0].trigger("click"); await wrapper.vm.$nextTick(); // Check that runner is no longer selected (internal state) expect(runnerCards[0].props("isSelected")).toBe(false); // Catcher summary pill remains visible expect(wrapper.find(".catcher-pill").exists()).toBe(true); }); it("switches selection when clicking different runner", async () => { const gameStore = useGameStore(); gameStore.setGameState({ id: 1, home_team_id: 1, away_team_id: 2, status: "active", inning: 1, half: "top", outs: 0, home_score: 0, away_score: 0, home_team_abbrev: "NYY", away_team_abbrev: "BOS", home_team_dice_color: "3b82f6", current_batter: null, current_pitcher: null, on_first: null, on_second: null, on_third: null, decision_phase: "idle", play_count: 0, }); gameStore.updateLineup(1, [ { id: 1, lineup_id: 1, team_id: 1, batting_order: 1, position: "LF", is_active: true, player: { id: 101, name: "Mike Trout", image: "https://example.com/trout.jpg", }, }, { id: 2, lineup_id: 2, team_id: 1, batting_order: 2, position: "CF", is_active: true, player: { id: 102, name: "Aaron Judge", image: "https://example.com/judge.jpg", }, }, ]); const wrapper = mount(RunnersOnBase, { global: { plugins: [pinia] }, props: { runners: { first: mockRunnerFirst, second: mockRunnerSecond, third: null, }, fieldingLineup: mockFieldingLineup, battingTeamColor: "#3b82f6", fieldingTeamColor: "#10b981", battingTeamAbbrev: "BOS", fieldingTeamAbbrev: "NYY", }, }); const runnerCards = wrapper.findAllComponents(RunnerCard); // Lead runner (2nd base, which is index 1 in 3B-2B-1B order) is auto-selected on mount await wrapper.vm.$nextTick(); expect(runnerCards[1].props("isSelected")).toBe(true); // 2B auto-selected // Click 1B runner (index 2) await runnerCards[2].trigger("click"); expect(runnerCards[1].props("isSelected")).toBe(false); // 2B deselected expect(runnerCards[2].props("isSelected")).toBe(true); // 1B selected // Click 2B runner again (index 1) await runnerCards[1].trigger("click"); expect(runnerCards[2].props("isSelected")).toBe(false); // 1B deselected expect(runnerCards[1].props("isSelected")).toBe(true); // 2B selected }); }); describe("team information", () => { it("displays team abbreviations when provided", async () => { const gameStore = useGameStore(); gameStore.setGameState({ id: 1, home_team_id: 1, away_team_id: 2, status: "active", inning: 1, half: "top", outs: 0, home_score: 0, away_score: 0, home_team_abbrev: "NYY", away_team_abbrev: "BOS", home_team_dice_color: "3b82f6", current_batter: null, current_pitcher: null, on_first: null, on_second: null, on_third: null, decision_phase: "idle", play_count: 0, }); gameStore.updateLineup(1, [ { id: 1, lineup_id: 1, team_id: 1, batting_order: 1, position: "LF", is_active: true, player: { id: 101, name: "Mike Trout", image: "https://example.com/trout.jpg", }, }, ]); const wrapper = mount(RunnersOnBase, { global: { plugins: [pinia] }, props: { runners: { first: mockRunnerFirst, second: null, third: null, }, fieldingLineup: mockFieldingLineup, battingTeamColor: "#3b82f6", fieldingTeamColor: "#10b981", battingTeamAbbrev: "BOS", fieldingTeamAbbrev: "NYY", }, }); // Click runner to expand and show team abbreviation const runnerCards = wrapper.findAllComponents(RunnerCard); await runnerCards[0].trigger("click"); expect(wrapper.text()).toContain("NYY"); }); it("uses default colors when not provided", () => { const wrapper = mount(RunnersOnBase, { global: { plugins: [pinia] }, props: { runners: { first: mockRunnerFirst, second: null, third: null, }, fieldingLineup: mockFieldingLineup, }, }); const runnerCards = wrapper.findAllComponents(RunnerCard); // Runner cards now use red (#ef4444) instead of blue expect(runnerCards[0].props("teamColor")).toBe("#ef4444"); }); }); });