strat-gameplay-webapp/frontend-sba/tests/unit/components/Decisions/DefensiveSetup.spec.ts

310 lines
10 KiB
TypeScript

import { describe, it, expect, beforeEach } from "vitest";
import { mount } from "@vue/test-utils";
import DefensiveSetup from "~/components/Decisions/DefensiveSetup.vue";
import { useDefensiveSetup } from "~/composables/useDefensiveSetup";
import type { DefensiveDecision } from "~/types/game";
describe("DefensiveSetup", () => {
const defaultProps = {
gameId: "test-game-123",
isActive: true,
};
beforeEach(() => {
// Reset the singleton composable state before each test
const { reset } = useDefensiveSetup();
reset();
});
describe("Rendering", () => {
it("renders component with header", () => {
const wrapper = mount(DefensiveSetup, {
props: defaultProps,
});
expect(wrapper.text()).toContain("Defensive Setup");
});
it("shows opponent turn indicator when not active", () => {
const wrapper = mount(DefensiveSetup, {
props: {
...defaultProps,
isActive: false,
},
});
expect(wrapper.text()).toContain("Opponent's Turn");
});
it("renders all form sections", () => {
const wrapper = mount(DefensiveSetup, {
props: defaultProps,
});
expect(wrapper.text()).toContain("Infield Depth");
expect(wrapper.text()).toContain("Outfield Depth");
expect(wrapper.text()).toContain("Hold Runners");
});
it("shows hint text directing users to runner pills", () => {
const wrapper = mount(DefensiveSetup, {
props: defaultProps,
});
expect(wrapper.text()).toContain(
"Tap the H icons on the runner pills above",
);
});
});
describe("Initial Values", () => {
it("uses default values when no currentSetup provided", () => {
const wrapper = mount(DefensiveSetup, {
props: defaultProps,
});
// Check preview shows defaults
expect(wrapper.text()).toContain("Normal");
});
it("syncs composable from provided currentSetup via watcher", async () => {
/**
* When currentSetup prop is provided, the component should sync the
* composable state to match it. This verifies the prop->composable sync.
*/
const currentSetup: DefensiveDecision = {
infield_depth: "normal",
outfield_depth: "normal",
hold_runners: [1, 3],
};
mount(DefensiveSetup, {
props: {
...defaultProps,
currentSetup,
},
});
// The composable should be synced from the prop via the watcher
const { holdRunnersArray, infieldDepth, outfieldDepth } =
useDefensiveSetup();
// Watcher fires on prop change, check initial sync happens
expect(infieldDepth.value).toBe("normal");
expect(outfieldDepth.value).toBe("normal");
});
});
describe("Hold Runners Display", () => {
it('shows "None" when no runners held', () => {
const wrapper = mount(DefensiveSetup, {
props: defaultProps,
});
expect(wrapper.text()).toContain("None");
});
it("shows held bases as amber badges when runners are held", () => {
/**
* When the composable has held runners, the DefensiveSetup should
* display them as read-only amber pill badges.
*/
const { syncFromDecision } = useDefensiveSetup();
syncFromDecision({
infield_depth: "normal",
outfield_depth: "normal",
hold_runners: [1, 3],
});
const wrapper = mount(DefensiveSetup, {
props: defaultProps,
});
expect(wrapper.text()).toContain("1st");
expect(wrapper.text()).toContain("3rd");
// Verify amber badges exist
const badges = wrapper.findAll(".bg-amber-100");
expect(badges.length).toBe(2);
});
it("displays holding status in preview for multiple runners", () => {
/**
* The preview section should show a comma-separated list of held bases.
*/
const { syncFromDecision } = useDefensiveSetup();
syncFromDecision({
infield_depth: "normal",
outfield_depth: "normal",
hold_runners: [1, 2, 3],
});
const wrapper = mount(DefensiveSetup, {
props: defaultProps,
});
expect(wrapper.text()).toContain("1st");
expect(wrapper.text()).toContain("2nd");
expect(wrapper.text()).toContain("3rd");
});
});
describe("Preview Display", () => {
it("displays current infield depth in preview", () => {
const { syncFromDecision } = useDefensiveSetup();
syncFromDecision({
infield_depth: "infield_in",
outfield_depth: "normal",
hold_runners: [],
});
const wrapper = mount(DefensiveSetup, {
props: {
...defaultProps,
gameState: {
on_third: 123, // Need runner on third for infield_in option
} as any,
},
});
expect(wrapper.text()).toContain("Infield In");
});
});
describe("Form Submission", () => {
it("emits submit event with composable state", async () => {
/**
* On submit, the component should call getDecision() from the composable
* and emit the full DefensiveDecision.
*/
const { syncFromDecision } = useDefensiveSetup();
syncFromDecision({
infield_depth: "normal",
outfield_depth: "normal",
hold_runners: [2],
});
const wrapper = mount(DefensiveSetup, {
props: defaultProps,
});
await wrapper.find("form").trigger("submit.prevent");
expect(wrapper.emitted("submit")).toBeTruthy();
const emitted = wrapper.emitted(
"submit",
)![0][0] as DefensiveDecision;
expect(emitted.infield_depth).toBe("normal");
expect(emitted.outfield_depth).toBe("normal");
expect(emitted.hold_runners).toEqual([2]);
});
it("does not submit when not active", async () => {
const wrapper = mount(DefensiveSetup, {
props: {
...defaultProps,
isActive: false,
},
});
await wrapper.find("form").trigger("submit.prevent");
expect(wrapper.emitted("submit")).toBeFalsy();
});
it("allows submit with default setup", async () => {
/**
* Submitting with defaults should emit a valid DefensiveDecision
* with normal depth and no held runners.
*/
const wrapper = mount(DefensiveSetup, {
props: defaultProps,
});
await wrapper.find("form").trigger("submit.prevent");
expect(wrapper.emitted("submit")).toBeTruthy();
const emitted = wrapper.emitted(
"submit",
)![0][0] as DefensiveDecision;
expect(emitted.infield_depth).toBe("normal");
expect(emitted.outfield_depth).toBe("normal");
expect(emitted.hold_runners).toEqual([]);
});
it("shows loading state during submission", async () => {
const wrapper = mount(DefensiveSetup, {
props: defaultProps,
});
// Trigger submission
wrapper.vm.submitting = true;
await wrapper.vm.$nextTick();
// Verify button is in loading state
expect(wrapper.vm.submitting).toBe(true);
});
});
describe("Submit Button State", () => {
it('shows "Wait for Your Turn" when not active', () => {
const wrapper = mount(DefensiveSetup, {
props: {
...defaultProps,
isActive: false,
},
});
expect(wrapper.vm.submitButtonText).toBe("Wait for Your Turn");
});
it('shows "Submit Defensive Setup" when active', () => {
const wrapper = mount(DefensiveSetup, {
props: defaultProps,
});
expect(wrapper.vm.submitButtonText).toBe("Submit Defensive Setup");
});
});
describe("Prop Updates", () => {
it("syncs composable state when currentSetup prop changes", async () => {
/**
* When the parent updates the currentSetup prop (e.g. from server state),
* the composable should be synced to match.
*/
const wrapper = mount(DefensiveSetup, {
props: defaultProps,
});
const newSetup: DefensiveDecision = {
infield_depth: "infield_in",
outfield_depth: "normal",
hold_runners: [1, 2, 3],
};
await wrapper.setProps({ currentSetup: newSetup });
const { infieldDepth, outfieldDepth, holdRunnersArray } =
useDefensiveSetup();
expect(infieldDepth.value).toBe("infield_in");
expect(outfieldDepth.value).toBe("normal");
expect(holdRunnersArray.value).toEqual([1, 2, 3]);
});
});
describe("Disabled State", () => {
it("disables depth controls when not active", () => {
const wrapper = mount(DefensiveSetup, {
props: {
...defaultProps,
isActive: false,
},
});
const buttonGroups = wrapper.findAllComponents({
name: "ButtonGroup",
});
buttonGroups.forEach((bg) => {
expect(bg.props("disabled")).toBe(true);
});
});
});
});