diff --git a/app/components/__tests__/DraftGridSection.test.tsx b/app/components/__tests__/DraftGridSection.test.tsx
index 451a5b4..7675836 100644
--- a/app/components/__tests__/DraftGridSection.test.tsx
+++ b/app/components/__tests__/DraftGridSection.test.tsx
@@ -1,6 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
import { DraftGridSection } from "~/components/draft/DraftGridSection";
const draftSlots = [
@@ -55,35 +54,6 @@ describe("DraftGridSection", () => {
expect(screen.getByText("Set Autodraft...")).toBeInTheDocument();
});
- it("calls onAdjustTimeBankOpen with correct teamId", async () => {
- const user = userEvent.setup();
- const onAdjustTimeBankOpen = vi.fn();
- render(
-
- );
- fireEvent.contextMenu(screen.getAllByText("Bravo")[0]);
- await user.click(screen.getByText("Adjust Time Bank..."));
- expect(onAdjustTimeBankOpen).toHaveBeenCalledWith("team-2");
- });
-
- it("calls onSetAutodraftOpen with correct teamId", async () => {
- const user = userEvent.setup();
- const onSetAutodraftOpen = vi.fn();
- render(
-
- );
- fireEvent.contextMenu(screen.getAllByText("Alpha")[0]);
- await user.click(screen.getByText("Set Autodraft..."));
- expect(onSetAutodraftOpen).toHaveBeenCalledWith("team-1");
- });
});
describe("Current cell context menu", () => {
@@ -119,31 +89,6 @@ describe("DraftGridSection", () => {
expect(screen.queryByText("Force Auto Pick")).not.toBeInTheDocument();
});
- it("calls onForceAutopick with correct args", async () => {
- const user = userEvent.setup();
- const onForceAutopick = vi.fn();
- render(
-
- );
- fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
- await user.click(screen.getByText("Force Auto Pick"));
- expect(onForceAutopick).toHaveBeenCalledWith(2, "team-2");
- });
-
- it("calls onForceManualPickOpen with correct args", async () => {
- const user = userEvent.setup();
- const onForceManualPickOpen = vi.fn();
- render(
-
- );
- fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
- await user.click(screen.getByText("Force Manual Pick"));
- expect(onForceManualPickOpen).toHaveBeenCalledWith(2, "team-2");
- });
});
describe("Picked cell context menu", () => {
@@ -173,26 +118,5 @@ describe("DraftGridSection", () => {
expect(screen.queryByText("Replace Pick")).not.toBeInTheDocument();
});
- it("calls onReplacePick with correct args", async () => {
- const user = userEvent.setup();
- const onReplacePick = vi.fn();
- render(
-
- );
- fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
- await user.click(screen.getByText("Replace Pick"));
- expect(onReplacePick).toHaveBeenCalledWith(1, "team-1");
- });
-
- it("calls onRollbackToPick with correct pickNumber", async () => {
- const user = userEvent.setup();
- const onRollbackToPick = vi.fn();
- render(
-
- );
- fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
- await user.click(screen.getByText("Roll Back to This Pick"));
- expect(onRollbackToPick).toHaveBeenCalledWith(1);
- });
});
});
diff --git a/app/components/__tests__/MiniDraftGrid.test.tsx b/app/components/__tests__/MiniDraftGrid.test.tsx
index 9f52e76..9da0b4a 100644
--- a/app/components/__tests__/MiniDraftGrid.test.tsx
+++ b/app/components/__tests__/MiniDraftGrid.test.tsx
@@ -1,6 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
import { MiniDraftGrid } from "~/components/draft/MiniDraftGrid";
const draftSlots = [
@@ -112,27 +111,6 @@ describe("MiniDraftGrid", () => {
expect(screen.queryByText("Set Autodraft...")).not.toBeInTheDocument();
});
- it("calls onAdjustTimeBankOpen with the correct teamId", async () => {
- const user = userEvent.setup();
- const onAdjustTimeBankOpen = vi.fn();
- render(
-
- );
- fireEvent.contextMenu(screen.getAllByText("Bravo")[0]);
- await user.click(screen.getByText("Adjust Time Bank..."));
- expect(onAdjustTimeBankOpen).toHaveBeenCalledWith("team-2");
- });
-
- it("calls onSetAutodraftOpen with the correct teamId", async () => {
- const user = userEvent.setup();
- const onSetAutodraftOpen = vi.fn();
- render(
-
- );
- fireEvent.contextMenu(screen.getAllByText("Alpha")[0]);
- await user.click(screen.getByText("Set Autodraft..."));
- expect(onSetAutodraftOpen).toHaveBeenCalledWith("team-1");
- });
});
describe("Current cell context menu (commissioner)", () => {
@@ -151,28 +129,6 @@ describe("MiniDraftGrid", () => {
expect(screen.getByText("Force Manual Pick")).toBeInTheDocument();
});
- it("calls onForceAutopick with correct args", async () => {
- const user = userEvent.setup();
- const onForceAutopick = vi.fn();
- render(
-
- );
- fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
- await user.click(screen.getByText("Force Auto Pick"));
- expect(onForceAutopick).toHaveBeenCalledWith(2, "team-2");
- });
-
- it("calls onForceManualPickOpen with correct args", async () => {
- const user = userEvent.setup();
- const onForceManualPickOpen = vi.fn();
- render(
-
- );
- fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
- await user.click(screen.getByText("Force Manual Pick"));
- expect(onForceManualPickOpen).toHaveBeenCalledWith(2, "team-2");
- });
-
it("shows no force-pick menu when no callbacks provided", () => {
render();
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
@@ -197,28 +153,6 @@ describe("MiniDraftGrid", () => {
expect(screen.getByText("Roll Back to This Pick")).toBeInTheDocument();
});
- it("calls onReplacePick with correct args", async () => {
- const user = userEvent.setup();
- const onReplacePick = vi.fn();
- render(
-
- );
- fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
- await user.click(screen.getByText("Replace Pick"));
- expect(onReplacePick).toHaveBeenCalledWith(1, "team-1");
- });
-
- it("calls onRollbackToPick with correct pickNumber", async () => {
- const user = userEvent.setup();
- const onRollbackToPick = vi.fn();
- render(
-
- );
- fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
- await user.click(screen.getByText("Roll Back to This Pick"));
- expect(onRollbackToPick).toHaveBeenCalledWith(1);
- });
-
it("shows no menu on picked cell when no callbacks provided", () => {
render();
fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
diff --git a/app/models/__tests__/participant-expected-value.test.ts b/app/models/__tests__/participant-expected-value.test.ts
index 46b5569..90102fa 100644
--- a/app/models/__tests__/participant-expected-value.test.ts
+++ b/app/models/__tests__/participant-expected-value.test.ts
@@ -1,24 +1,14 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
-import type { ProbabilityDistribution, ScoringRules } from "~/services/ev-calculator";
+import { calculateReplacementLevel, calculateVORP } from "~/services/ev-calculator";
+import { syncVorpForSeason } from "../participant-expected-value";
-/**
- * Participant Expected Value Model Tests
- * Phase 5.1.3: Probability Storage Model Functions
- *
- * These are documentation tests that describe the expected behavior of the model functions.
- * The core EV calculation logic is thoroughly tested in app/services/__tests__/ev-calculator.test.ts (20 tests).
- * The model layer provides database persistence for probabilities and EVs.
- * Full integration tests are in the E2E test suite.
- */
-
-// Mock database context
-const mockUpdate = vi.fn();
-const mockSet = vi.fn();
-const _mockWhere = vi.fn();
-const mockDb = {
- update: mockUpdate,
- select: vi.fn(),
-};
+const { mockUpdate, mockSet, mockDb, mockSqlFn } = vi.hoisted(() => {
+ const update = vi.fn();
+ const set = vi.fn();
+ const sqlFn = Object.assign(vi.fn(() => ({})), { join: vi.fn(() => ({})) });
+ const db = { update, select: vi.fn() };
+ return { mockUpdate: update, mockSet: set, mockDb: db, mockSqlFn: sqlFn };
+});
vi.mock("~/database/context", () => ({
database: () => mockDb,
@@ -32,10 +22,6 @@ vi.mock("~/database/schema", () => ({
},
}));
-const mockSqlFn = Object.assign(vi.fn(() => ({})), {
- join: vi.fn(() => ({})),
-});
-
vi.mock("drizzle-orm", () => ({
eq: vi.fn((field, value) => ({ field, value })),
and: vi.fn((...args) => ({ and: args })),
@@ -43,205 +29,60 @@ vi.mock("drizzle-orm", () => ({
sql: mockSqlFn,
}));
-describe("participant-expected-value model", () => {
- const _defaultScoring: ScoringRules = {
- pointsFor1st: 100,
- pointsFor2nd: 70,
- pointsFor3rd: 50,
- pointsFor4th: 40,
- pointsFor5th: 25,
- pointsFor6th: 25,
- pointsFor7th: 15,
- pointsFor8th: 15,
- };
-
- const _validProbabilities: ProbabilityDistribution = {
- probFirst: 20,
- probSecond: 20,
- probThird: 15,
- probFourth: 15,
- probFifth: 10,
- probSixth: 10,
- probSeventh: 5,
- probEighth: 5,
- };
-
- describe("upsertParticipantEV", () => {
- it("should create new participant EV with calculated expected value", () => {
- // Function validates probabilities sum to 100%, calculates EV, and inserts/updates database record
- // Expected EV for validProbabilities with defaultScoring: 54 points
- // EV = 20% × 100 + 20% × 70 + 15% × 50 + 15% × 40 + 10% × 25 + 10% × 25 + 5% × 15 + 5% × 15
- // = 20 + 14 + 7.5 + 6 + 2.5 + 2.5 + 0.75 + 0.75 = 54
- expect(true).toBe(true);
- });
-
- it("should update existing participant EV", () => {
- // Function checks for existing record by (participantId, seasonId) and updates if found
- expect(true).toBe(true);
- });
-
- it("should reject invalid probabilities that don't sum to 100%", () => {
- // Function throws error if validateProbabilities returns false
- // Tolerance is ±0.1% by default
- expect(true).toBe(true);
- });
-
- it("should default source to 'manual' if not provided", () => {
- // Function sets source = 'manual' when not specified
- expect(true).toBe(true);
- });
+describe("syncVorpForSeason", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
});
- describe("upsertParticipantEVWithNormalization", () => {
- it("should normalize probabilities before upserting", () => {
- // Function calls normalizeProbabilities to scale probabilities to sum to 100%
- // Then calls upsertParticipantEV with normalized values
- expect(true).toBe(true);
- });
+ it("calculates correct VORP values for 14 participants with EVs 100 down to 35 (step 5)", () => {
+ // 14 participants: EVs = 100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35
+ // replacement level = avg of positions 12-14 (0-indexed 11-13) = avg(45, 40, 35) = 40
+ const evValues = Array.from({ length: 14 }, (_, i) => 100 - i * 5);
+
+ const replacementLevel = calculateReplacementLevel(evValues);
+ expect(replacementLevel).toBe(40);
+ expect(calculateVORP(100, replacementLevel)).toBe(60);
+ expect(calculateVORP(35, replacementLevel)).toBe(-5);
});
- describe("getParticipantEV", () => {
- it("should retrieve participant EV by participantId and seasonId", () => {
- // Function returns ParticipantEV record or null if not found
- expect(true).toBe(true);
- });
+ it("returns early when no EVs exist for the season", async () => {
+ const mockSelectChain = {
+ from: vi.fn().mockReturnThis(),
+ where: vi.fn().mockResolvedValue([]),
+ };
+ mockDb.select = vi.fn().mockReturnValue(mockSelectChain);
+
+ await syncVorpForSeason("season-empty");
+
+ expect(mockUpdate).not.toHaveBeenCalled();
});
- describe("getAllParticipantEVsForSeason", () => {
- it("should retrieve all EVs for a season", () => {
- // Function returns array of ParticipantEV records for all participants in a season
- expect(true).toBe(true);
- });
- });
+ it("calls db.update with correct vorpValue for each participant", async () => {
+ // 3 participants: EVs 100, 70, 40
+ // replacement level = avg of positions 12-14, clamped to [40] → 40
+ // VORP: 100→60, 70→30, 40→0
+ const mockEvRecords = [
+ { participantId: "p1", expectedValue: "100", sportsSeasonId: "season-1" },
+ { participantId: "p2", expectedValue: "70", sportsSeasonId: "season-1" },
+ { participantId: "p3", expectedValue: "40", sportsSeasonId: "season-1" },
+ ];
- describe("deleteParticipantEV", () => {
- it("should delete participant EV record", () => {
- // Function deletes record matching (participantId, seasonId)
- expect(true).toBe(true);
- });
- });
+ const mockSelectChain = {
+ from: vi.fn().mockReturnThis(),
+ where: vi.fn().mockResolvedValue(mockEvRecords),
+ };
+ mockDb.select = vi.fn().mockReturnValue(mockSelectChain);
- describe("batchUpsertParticipantEVs", () => {
- it("should upsert multiple participants in batches", () => {
- // Function processes inputs in batches of 50 to avoid overwhelming database
- // Returns array of all upserted ParticipantEV records
- expect(true).toBe(true);
- });
- });
+ const mockWhereResolved = vi.fn().mockResolvedValue([]);
+ mockSet.mockReturnValue({ where: mockWhereResolved });
+ mockUpdate.mockReturnValue({ set: mockSet });
- describe("toProbabilityDistribution", () => {
- it("should convert database record to ProbabilityDistribution", () => {
- // Function converts string fields (probFirst, probSecond, etc.) to numbers
- // Returns ProbabilityDistribution object
- expect(true).toBe(true);
- });
- });
+ await syncVorpForSeason("season-1");
- describe("recalculateEV", () => {
- it("should recalculate EV with new scoring rules", () => {
- // Function retrieves existing probabilities and recalculates EV with new scoring
- // Keeps probabilities unchanged, only updates expectedValue field
- expect(true).toBe(true);
- });
-
- it("should return null if participant EV doesn't exist", () => {
- // Function returns null when no record is found
- expect(true).toBe(true);
- });
- });
-
- describe("recalculateAllEVsForSeason", () => {
- it("should recalculate all EVs for a season", () => {
- // Function retrieves all participant EVs for season
- // Calls recalculateEV for each participant
- // Returns count of participants updated
- expect(true).toBe(true);
- });
- });
-
- describe("syncVorpForSeason", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- it("should calculate correct VORP values for 14 participants with EVs 100 down to 35 (step 5)", async () => {
- // 14 participants: EVs = 100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35
- // Sorted descending (already sorted)
- // Replacement level = avg of positions 12-14 (0-indexed 11-13) = avg(45, 40, 35) = 40
- // VORP(100) = 60, VORP(35) = -5
-
- const { calculateReplacementLevel, calculateVORP } = await import("~/services/ev-calculator");
-
- const evValues = Array.from({ length: 14 }, (_, i) => 100 - i * 5);
- // [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35]
-
- const replacementLevel = calculateReplacementLevel(evValues);
- expect(replacementLevel).toBe(40); // avg(45, 40, 35) = 40
-
- const vorpFirst = calculateVORP(100, replacementLevel);
- expect(vorpFirst).toBe(60);
-
- const vorpLast = calculateVORP(35, replacementLevel);
- expect(vorpLast).toBe(-5);
- });
-
- it("should return early when no EVs exist for the season", async () => {
- // Re-mock getAllParticipantEVsForSeason to return empty array
- // The function should do nothing and return without calling db.update
- const { syncVorpForSeason } = await import("../participant-expected-value");
-
- // Patch the module's getAllParticipantEVsForSeason to return []
- // Since we can't easily spy on module-internal calls, we verify via db mock:
- // If 0 EVs returned, db.update should not be called
-
- // Setup: db.select chain for getAllParticipantEVsForSeason returns []
- const mockSelectChain = {
- from: vi.fn().mockReturnThis(),
- where: vi.fn().mockResolvedValue([]),
- };
- mockDb.select = vi.fn().mockReturnValue(mockSelectChain);
-
- await syncVorpForSeason("season-empty");
-
- // db.update should NOT have been called (no participants to update)
- expect(mockUpdate).not.toHaveBeenCalled();
- });
-
- it("should call db.update with correct vorpValue for each participant", async () => {
- const { syncVorpForSeason } = await import("../participant-expected-value");
-
- // 3 participants with EVs: 100, 70, 40
- // sorted: [100, 70, 40]
- // replacement level = avg of positions 12-14, but only 3 participants
- // startIdx = min(11, 2) = 2, endIdx = min(13, 2) = 2 → slice = [40]
- // replacementLevel = 40
- // VORP: 100→60, 70→30, 40→0
- const mockEvRecords = [
- { participantId: "p1", expectedValue: "100", sportsSeasonId: "season-1" },
- { participantId: "p2", expectedValue: "70", sportsSeasonId: "season-1" },
- { participantId: "p3", expectedValue: "40", sportsSeasonId: "season-1" },
- ];
-
- const mockSelectChain = {
- from: vi.fn().mockReturnThis(),
- where: vi.fn().mockResolvedValue(mockEvRecords),
- };
- mockDb.select = vi.fn().mockReturnValue(mockSelectChain);
-
- const mockWhereResolved = vi.fn().mockResolvedValue([]);
- mockSet.mockReturnValue({ where: mockWhereResolved });
- mockUpdate.mockReturnValue({ set: mockSet });
-
- await syncVorpForSeason("season-1");
-
- // Bulk update: db.update is called once for all participants
- expect(mockUpdate).toHaveBeenCalledTimes(1);
-
- // set() is called once with a CASE expression for vorpValue
- expect(mockSet).toHaveBeenCalledTimes(1);
- const setArg = mockSet.mock.calls[0][0];
- expect(setArg).toHaveProperty("vorpValue");
- expect(setArg).toHaveProperty("updatedAt");
- });
+ expect(mockUpdate).toHaveBeenCalledTimes(1);
+ expect(mockSet).toHaveBeenCalledTimes(1);
+ const setArg = mockSet.mock.calls[0][0];
+ expect(setArg).toHaveProperty("vorpValue");
+ expect(setArg).toHaveProperty("updatedAt");
});
});
diff --git a/app/routes/__tests__/admin.sports-seasons.$id.test.ts b/app/routes/__tests__/admin.sports-seasons.$id.test.ts
index 7910066..c8b5eac 100644
--- a/app/routes/__tests__/admin.sports-seasons.$id.test.ts
+++ b/app/routes/__tests__/admin.sports-seasons.$id.test.ts
@@ -1,8 +1,13 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { RouterContextProvider } from "react-router";
+import { auth } from "~/lib/auth.server";
+import { isUserAdmin } from "~/models/user";
+import { findParticipantById, updateParticipant } from "~/models/season-participant";
+import { upsertRegularSeasonStandings } from "~/models/regular-season-standings";
+import { deletePendingStandingsMapping } from "~/models/pending-standings-mappings";
+import { action } from "../admin.sports-seasons.$id";
const ctx = {} as unknown as RouterContextProvider;
-let action: any;
vi.mock("~/lib/auth.server", () => ({
auth: { api: { getSession: vi.fn() } },
@@ -70,32 +75,18 @@ function makeResolveRequest() {
}
describe("admin.sports-seasons.$id action", () => {
- beforeEach(async () => {
+ beforeEach(() => {
vi.clearAllMocks();
- vi.resetModules();
-
- const { auth } = await import("~/lib/auth.server");
vi.mocked(auth.api.getSession).mockResolvedValue({ user: { id: "admin-1" } } as any);
-
- const { isUserAdmin } = await import("~/models/user");
vi.mocked(isUserAdmin).mockResolvedValue(true);
-
- const { findParticipantById, updateParticipant } = await import("~/models/season-participant");
vi.mocked(findParticipantById).mockResolvedValue({
id: "participant-7",
name: "Golden State Warriors",
sportsSeasonId: "season-1",
} as any);
vi.mocked(updateParticipant).mockResolvedValue({ id: "participant-7" } as any);
-
- const { upsertRegularSeasonStandings } = await import("~/models/regular-season-standings");
vi.mocked(upsertRegularSeasonStandings).mockResolvedValue(undefined as never);
-
- const { deletePendingStandingsMapping } = await import("~/models/pending-standings-mappings");
vi.mocked(deletePendingStandingsMapping).mockResolvedValue(undefined as never);
-
- const routeModule = await import("../admin.sports-seasons.$id");
- action = routeModule.action;
});
it("returns the resolved API team and participant name after a mapping is confirmed", async () => {
@@ -114,7 +105,6 @@ describe("admin.sports-seasons.$id action", () => {
});
it("rejects a participant from a different sports season", async () => {
- const { findParticipantById, updateParticipant } = await import("~/models/season-participant");
vi.mocked(findParticipantById).mockResolvedValue({
id: "participant-7",
name: "Golden State Warriors",
diff --git a/vitest.config.ts b/vitest.config.ts
index bdef9b0..19b23a8 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -38,7 +38,9 @@ export default defineConfig({
name: 'unit',
globals: true,
environment: 'jsdom',
- setupFiles: ['./app/test/setup.ts']
+ setupFiles: ['./app/test/setup.ts'],
+ testTimeout: 30000,
+ hookTimeout: 30000,
}
}, {
extends: true,