import { describe, it, expect, beforeEach } from "vitest"; import { mount } from "@vue/test-utils"; import { createPinia, setActivePinia } from "pinia"; import RunnerCard from "~/components/Game/RunnerCard.vue"; import { useGameStore } from "~/store/game"; import type { LineupPlayerState } from "~/types/game"; describe("RunnerCard", () => { let pinia: ReturnType; beforeEach(() => { pinia = createPinia(); setActivePinia(pinia); }); const mockRunner: LineupPlayerState = { lineup_id: 1, batting_order: 1, position: "LF", card_id: 101, }; describe("empty base state", () => { it("renders empty state when no runner provided", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: null, isSelected: false, teamColor: "#3b82f6", }, }); expect(wrapper.find(".runner-pill.empty").exists()).toBe(true); expect(wrapper.text()).toContain("Empty"); }); it("displays base label for empty base", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "2B", runner: null, isSelected: false, teamColor: "#3b82f6", }, }); expect(wrapper.text()).toContain("2B"); }); it("shows hollow circle for empty base", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "3B", runner: null, isSelected: false, teamColor: "#3b82f6", }, }); const circle = wrapper.find(".rounded-full.border-dashed"); expect(circle.exists()).toBe(true); }); it("does not emit click event for empty base", async () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: null, isSelected: false, teamColor: "#3b82f6", }, }); await wrapper.trigger("click"); expect(wrapper.emitted("click")).toBeUndefined(); }); }); describe("occupied base state", () => { beforeEach(() => { const gameStore = useGameStore(); // Set game state first so updateLineup knows the team ID 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", headshot: "https://example.com/trout-headshot.jpg", }, }, ]); }); it("renders occupied state when runner provided", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", }, }); expect(wrapper.find(".runner-pill.occupied").exists()).toBe(true); expect(wrapper.find(".runner-pill.empty").exists()).toBe(false); }); it("displays runner name", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", }, }); expect(wrapper.text()).toContain("Mike Trout"); }); it("displays base label", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "2B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", }, }); expect(wrapper.text()).toContain("2B"); }); it("displays player headshot when available", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", }, }); const img = wrapper.find('img[alt="Mike Trout"]'); expect(img.exists()).toBe(true); expect(img.attributes("src")).toBe( "https://example.com/trout-headshot.jpg", ); }); it("applies team color to border", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#ff0000", }, }); const avatar = wrapper.find(".rounded-full.border-2"); expect(avatar.attributes("style")).toContain( "border-color: #ff0000", ); }); it("emits click event when clicked", async () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", }, }); await wrapper.trigger("click"); expect(wrapper.emitted("click")).toHaveLength(1); }); }); describe("selected state", () => { beforeEach(() => { 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-card.jpg", headshot: "https://example.com/trout-headshot.jpg", }, }, ]); }); it("does not apply selected class when not selected", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", }, }); expect(wrapper.find(".runner-pill.selected").exists()).toBe(false); }); it("applies selected class when isSelected is true", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: true, teamColor: "#3b82f6", }, }); expect(wrapper.find(".runner-pill.selected").exists()).toBe(true); }); }); describe("player name handling", () => { it('shows "Unknown Runner" when player not found in store', () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", }, }); expect(wrapper.text()).toContain("Unknown Runner"); }); }); describe("hold runner icon", () => { it("does not show hold icon by default", () => { /** * When neither isHeld nor holdInteractive is set, the hold icon * should not appear — keeps the pill clean for non-defensive contexts. */ const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", }, }); expect(wrapper.find(".hold-icon").exists()).toBe(false); }); it("shows hold icon when holdInteractive is true", () => { /** * During the defensive decision phase, holdInteractive is true * and the icon should appear even when the runner is not held. */ const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", holdInteractive: true, isHeld: false, }, }); expect(wrapper.find(".hold-icon").exists()).toBe(true); }); it("shows hold icon when isHeld is true (read-only)", () => { /** * After submission, isHeld shows the current hold state as a * non-interactive indicator even when holdInteractive is false. */ const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", isHeld: true, holdInteractive: false, }, }); const icon = wrapper.find(".hold-icon"); expect(icon.exists()).toBe(true); expect(icon.attributes("disabled")).toBeDefined(); }); it("applies amber styling when held", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", isHeld: true, holdInteractive: true, }, }); const icon = wrapper.find(".hold-icon"); expect(icon.classes()).toContain("bg-amber-500"); }); it("applies gray styling when not held", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", isHeld: false, holdInteractive: true, }, }); const icon = wrapper.find(".hold-icon"); expect(icon.classes()).toContain("bg-gray-200"); }); it("emits toggleHold when clicked in interactive mode", async () => { /** * Clicking the hold icon should emit toggleHold so the parent * can update the composable state. */ const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", isHeld: false, holdInteractive: true, }, }); await wrapper.find(".hold-icon").trigger("click"); expect(wrapper.emitted("toggleHold")).toHaveLength(1); }); it("does not emit toggleHold when not interactive", async () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", isHeld: true, holdInteractive: false, }, }); await wrapper.find(".hold-icon").trigger("click"); expect(wrapper.emitted("toggleHold")).toBeUndefined(); }); it("does not emit click (selection) when hold icon is clicked", async () => { /** * The hold icon uses @click.stop so tapping it should NOT trigger * the pill's selection behavior — only the hold toggle. */ const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", isHeld: false, holdInteractive: true, }, }); await wrapper.find(".hold-icon").trigger("click"); expect(wrapper.emitted("toggleHold")).toHaveLength(1); expect(wrapper.emitted("click")).toBeUndefined(); }); it("applies held class to the pill when isHeld", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: mockRunner, isSelected: false, teamColor: "#3b82f6", isHeld: true, }, }); expect(wrapper.find(".runner-pill.held").exists()).toBe(true); }); it("does not show hold icon on empty bases", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: null, isSelected: false, teamColor: "#3b82f6", holdInteractive: true, }, }); expect(wrapper.find(".hold-icon").exists()).toBe(false); }); }); describe("base label variations", () => { it("displays 1B correctly", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "1B", runner: null, isSelected: false, teamColor: "#3b82f6", }, }); expect(wrapper.text()).toContain("1B"); }); it("displays 2B correctly", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "2B", runner: null, isSelected: false, teamColor: "#3b82f6", }, }); expect(wrapper.text()).toContain("2B"); }); it("displays 3B correctly", () => { const wrapper = mount(RunnerCard, { global: { plugins: [pinia] }, props: { base: "3B", runner: null, isSelected: false, teamColor: "#3b82f6", }, }); expect(wrapper.text()).toContain("3B"); }); }); });