strat-gameplay-webapp/frontend-sba/tests/unit/components/Game/RunnersOnBase.spec.ts
Cal Corum 46caf9cd81 CLAUDE: Update color scheme - red for runners, blue for catcher
Runner highlights and cards:
- Pills: red-500 ring, red-50 background when selected
- Full cards: red gradient (red-900 to red-950), red-600 border
- Pulse glow: red animation (rgba(239, 68, 68))
- Hardcoded red color (#ef4444) for runner pill borders

Catcher highlights and cards:
- Pill: blue-500 ring, blue-50 background when selected
- Full card: blue gradient (blue-900 to blue-950), blue-600 border
- Pulse glow: blue animation (rgba(59, 130, 246))

Updated tests to expect new colors

All 15 RunnersOnBase tests passing
All 16 RunnerCard tests passing
2026-02-07 23:50:24 -06:00

587 lines
21 KiB
TypeScript

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<typeof createPinia>;
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");
});
});
});