strat-gameplay-webapp/frontend-sba/tests/unit/components/Game/RunnerCard.spec.ts
Cal Corum 078516f682 CLAUDE: Add comprehensive tests for RunnersOnBase and RunnerCard components
- Test empty/occupied/expanded states
- Test player name handling and initials
- Test runner selection and catcher card expansion
- Test team color integration
- All 40 tests passing
2026-02-06 19:38:18 -06:00

607 lines
19 KiB
TypeScript

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<typeof createPinia>;
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,
isExpanded: false,
teamColor: "#3b82f6",
},
});
expect(wrapper.find(".runner-card.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,
isExpanded: 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,
isExpanded: 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,
isExpanded: 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,
isExpanded: false,
teamColor: "#3b82f6",
},
});
expect(wrapper.find(".runner-card.occupied").exists()).toBe(true);
expect(wrapper.find(".runner-card.empty").exists()).toBe(false);
});
it("displays runner name", () => {
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "1B",
runner: mockRunner,
isExpanded: 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,
isExpanded: false,
teamColor: "#3b82f6",
},
});
expect(wrapper.text()).toContain("2B");
});
it("displays runner number based on lineup_id", () => {
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "1B",
runner: mockRunner,
isExpanded: false,
teamColor: "#3b82f6",
},
});
expect(wrapper.text()).toContain("#01");
});
it("displays player headshot when available", () => {
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "1B",
runner: mockRunner,
isExpanded: 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,
isExpanded: false,
teamColor: "#ff0000",
},
});
const avatar = wrapper.find(".rounded-full.border-2");
expect(avatar.attributes("style")).toContain(
"border-color: #ff0000",
);
});
it("shows chevron icon when occupied", () => {
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "1B",
runner: mockRunner,
isExpanded: false,
teamColor: "#3b82f6",
},
});
const chevron = wrapper.find("svg");
expect(chevron.exists()).toBe(true);
});
it("emits click event when clicked", async () => {
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "1B",
runner: mockRunner,
isExpanded: false,
teamColor: "#3b82f6",
},
});
await wrapper.trigger("click");
expect(wrapper.emitted("click")).toHaveLength(1);
});
});
describe("expanded 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,
on_base_code: 0,
home_team: {
id: 1,
name: "Home Team",
abbreviation: "HOME",
dice_color: "3b82f6",
},
away_team: {
id: 2,
name: "Away Team",
abbreviation: "AWAY",
dice_color: "10b981",
},
});
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 show expanded view when collapsed", () => {
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "1B",
runner: mockRunner,
isExpanded: false,
teamColor: "#3b82f6",
},
});
expect(wrapper.find(".runner-expanded").exists()).toBe(false);
});
it("shows expanded view when isExpanded is true", () => {
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "1B",
runner: mockRunner,
isExpanded: true,
teamColor: "#3b82f6",
},
});
expect(wrapper.find(".runner-expanded").exists()).toBe(true);
});
it("displays full player card image when expanded", () => {
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "1B",
runner: mockRunner,
isExpanded: true,
teamColor: "#3b82f6",
},
});
const cardImg = wrapper.find(
'.runner-expanded img[alt="Mike Trout card"]',
);
expect(cardImg.exists()).toBe(true);
expect(cardImg.attributes("src")).toBe(
"https://example.com/trout-card.jpg",
);
});
it("shows player initials when no card image available", () => {
const gameStore = useGameStore();
gameStore.setGameState({
id: 1,
home_team_id: 1,
away_team_id: 2,
status: "active",
inning: 1,
half: "top",
outs: 0,
on_base_code: 0,
home_team: {
id: 1,
name: "Home Team",
abbreviation: "HOME",
dice_color: "3b82f6",
},
away_team: {
id: 2,
name: "Away Team",
abbreviation: "AWAY",
dice_color: "10b981",
},
});
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: "",
headshot: "",
},
},
]);
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "1B",
runner: mockRunner,
isExpanded: true,
teamColor: "#3b82f6",
},
});
expect(wrapper.text()).toContain("MT");
});
it('displays "RUNNER" label in expanded header', () => {
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "2B",
runner: mockRunner,
isExpanded: true,
teamColor: "#3b82f6",
},
});
expect(wrapper.find(".runner-expanded").text()).toContain("RUNNER");
});
it("applies expanded class when isExpanded is true", () => {
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "1B",
runner: mockRunner,
isExpanded: true,
teamColor: "#3b82f6",
},
});
expect(wrapper.find(".runner-card.expanded").exists()).toBe(true);
});
it("rotates chevron when expanded", () => {
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "1B",
runner: mockRunner,
isExpanded: true,
teamColor: "#3b82f6",
},
});
const chevron = wrapper.find("svg");
expect(chevron.classes()).toContain("rotate-90");
});
});
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,
isExpanded: false,
teamColor: "#3b82f6",
},
});
expect(wrapper.text()).toContain("Unknown Runner");
});
it("extracts initials from first and last name", () => {
const gameStore = useGameStore();
gameStore.setGameState({
id: 1,
home_team_id: 1,
away_team_id: 2,
status: "active",
inning: 1,
half: "top",
outs: 0,
on_base_code: 0,
home_team: {
id: 1,
name: "Home Team",
abbreviation: "HOME",
dice_color: "3b82f6",
},
away_team: {
id: 2,
name: "Away Team",
abbreviation: "AWAY",
dice_color: "10b981",
},
});
gameStore.updateLineup(1, [
{
id: 1,
lineup_id: 1,
team_id: 1,
batting_order: 1,
position: "LF",
is_active: true,
player: {
id: 101,
name: "Aaron Donald Judge",
image: "",
},
},
]);
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "1B",
runner: mockRunner,
isExpanded: true,
teamColor: "#3b82f6",
},
});
// Should use first and last name (A + J)
expect(wrapper.text()).toContain("AJ");
});
it("handles single-word names", () => {
const gameStore = useGameStore();
gameStore.setGameState({
id: 1,
home_team_id: 1,
away_team_id: 2,
status: "active",
inning: 1,
half: "top",
outs: 0,
on_base_code: 0,
home_team: {
id: 1,
name: "Home Team",
abbreviation: "HOME",
dice_color: "3b82f6",
},
away_team: {
id: 2,
name: "Away Team",
abbreviation: "AWAY",
dice_color: "10b981",
},
});
gameStore.updateLineup(1, [
{
id: 1,
lineup_id: 1,
team_id: 1,
batting_order: 1,
position: "LF",
is_active: true,
player: {
id: 101,
name: "Pele",
image: "",
},
},
]);
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "1B",
runner: mockRunner,
isExpanded: true,
teamColor: "#3b82f6",
},
});
expect(wrapper.text()).toContain("PE");
});
});
describe("base label variations", () => {
it("displays 1B correctly", () => {
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "1B",
runner: null,
isExpanded: false,
teamColor: "#3b82f6",
},
});
expect(wrapper.text()).toContain("1B");
});
it("displays 2B correctly", () => {
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "2B",
runner: null,
isExpanded: false,
teamColor: "#3b82f6",
},
});
expect(wrapper.text()).toContain("2B");
});
it("displays 3B correctly", () => {
const wrapper = mount(RunnerCard, {
global: { plugins: [pinia] },
props: {
base: "3B",
runner: null,
isExpanded: false,
teamColor: "#3b82f6",
},
});
expect(wrapper.text()).toContain("3B");
});
});
});