- Swap base order to 3B, 2B, 1B (left to right, closer to baseball diamond) - Auto-select lead runner on mount (priority: 3B > 2B > 1B) - Make catcher pill clickable to show catcher card only - Add 'catcher' as a selection option alongside runner bases - Update expanded view to handle catcher-only display (centered, single card) - Add toggleCatcher() function - Update tests for new base order and auto-selection behavior All 15 RunnersOnBase tests passing All 16 RunnerCard tests passing
585 lines
20 KiB
TypeScript
585 lines
20 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);
|
|
runnerCards.forEach((card) => {
|
|
expect(card.props("teamColor")).toBe("#ff0000");
|
|
});
|
|
});
|
|
});
|
|
|
|
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 is catcher (green), matchup-card-blue is runner (blue)
|
|
expect(wrapper.find(".matchup-card-blue").exists()).toBe(true); // Runner full card
|
|
expect(wrapper.find(".matchup-card").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-blue").exists()).toBe(true);
|
|
expect(wrapper.find(".matchup-card").exists()).toBe(true);
|
|
|
|
// 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);
|
|
expect(runnerCards[0].props("teamColor")).toBe("#3b82f6");
|
|
});
|
|
});
|
|
});
|