310 lines
10 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|
|
});
|