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("Current Setup"); }); }); 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 in preview', () => { const wrapper = mount(DefensiveSetup, { props: defaultProps, }); // Check preview section shows "None" for holding expect(wrapper.text()).toContain("Holding:None"); }); it("displays holding status in preview for held runners", () => { /** * The preview section should show a comma-separated list of held bases. * Hold runner UI has moved to the runner pills themselves. */ const { syncFromDecision } = useDefensiveSetup(); syncFromDecision({ infield_depth: "normal", outfield_depth: "normal", hold_runners: [1, 3], }); const wrapper = mount(DefensiveSetup, { props: defaultProps, }); // Preview should show the held bases expect(wrapper.text()).toContain("Holding:1st, 3rd"); }); 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("Holding:1st, 2nd, 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); }); }); }); });