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
587 lines
21 KiB
TypeScript
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");
|
|
});
|
|
});
|
|
});
|