* Improve admin dashboard stats and fix scoring event date bugs, fixes #92 - Add "Seasons by Status" breakdown card (pre-draft / drafting / active / completed) - Replace full-table fetches with COUNT queries for sports, sports seasons, and templates - Fix bracket events with null eventDate disappearing from the Games to Score tabs by computing a displayDate from matched playoffMatchGames.scheduledAt in getEventsForDates - Fix Today tab badge showing stale count when all events are scored - Hide "Getting Started" checklist once the system has sports configured - Use explicit "en-US" locale in formatEventTime for deterministic output Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix scoring-event-dashboard tests after select/sql changes - Rename mockSelectDistinct → mockSelect to match implementation change from .selectDistinct() to .select() - Add sql to drizzle-orm mock - Update mock row shapes to include eventDate and gameDate fields - Add two tests covering displayDate derivation from eventDate and gameDate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
273 lines
9.5 KiB
TypeScript
273 lines
9.5 KiB
TypeScript
/**
|
|
* Tests for getEventsForDates — the admin dashboard "Games to Score" query.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
// ── DB mock ──────────────────────────────────────────────────────────────────
|
|
// select returns a fluent chain; findMany is used in step 2.
|
|
const mockFindMany = vi.fn();
|
|
|
|
function makeSelectChain(rows: { id: string; eventDate?: string | null; gameDate?: string | null }[]) {
|
|
const chain = {
|
|
from: vi.fn().mockReturnThis(),
|
|
leftJoin: vi.fn().mockReturnThis(),
|
|
where: vi.fn().mockResolvedValue(rows),
|
|
};
|
|
return chain;
|
|
}
|
|
|
|
const mockSelect = vi.fn();
|
|
|
|
const mockDb = {
|
|
query: {
|
|
scoringEvents: { findMany: mockFindMany },
|
|
},
|
|
select: mockSelect,
|
|
};
|
|
|
|
vi.mock("~/database/context", () => ({
|
|
database: () => mockDb,
|
|
}));
|
|
|
|
vi.mock("~/database/schema", () => ({
|
|
scoringEvents: {
|
|
id: "se.id",
|
|
eventDate: "se.event_date",
|
|
eventType: "se.event_type",
|
|
eventStartsAt: "se.event_starts_at",
|
|
createdAt: "se.created_at",
|
|
},
|
|
playoffMatches: {
|
|
scoringEventId: "pm.scoring_event_id",
|
|
id: "pm.id",
|
|
},
|
|
playoffMatchGames: {
|
|
playoffMatchId: "pmg.playoff_match_id",
|
|
scheduledAt: "pmg.scheduled_at",
|
|
},
|
|
}));
|
|
|
|
vi.mock("drizzle-orm", () => ({
|
|
eq: (col: unknown, val: unknown) => ({ type: "eq", col, val }),
|
|
and: (...args: unknown[]) => ({ type: "and", args }),
|
|
asc: (col: unknown) => ({ type: "asc", col }),
|
|
desc: (col: unknown) => ({ type: "desc", col }),
|
|
gte: (col: unknown, val: unknown) => ({ type: "gte", col, val }),
|
|
lte: (col: unknown, val: unknown) => ({ type: "lte", col, val }),
|
|
or: (...args: unknown[]) => ({ type: "or", args }),
|
|
inArray: (col: unknown, arr: unknown) => ({ type: "inArray", col, arr }),
|
|
isNotNull: (col: unknown) => ({ type: "isNotNull", col }),
|
|
sql: (strings: TemplateStringsArray, ...values: unknown[]) => ({ type: "sql", strings, values }),
|
|
}));
|
|
|
|
import { getEventsForDates } from "../scoring-event";
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
function makeEventRow(overrides: Partial<{
|
|
id: string;
|
|
name: string;
|
|
eventDate: string | null;
|
|
eventStartsAt: Date | null;
|
|
eventType: string;
|
|
isComplete: boolean;
|
|
completedAt: Date | null;
|
|
sportsSeasonId: string;
|
|
sportsSeason: { name: string; sport: { name: string; slug: string | null } };
|
|
}> = {}) {
|
|
return {
|
|
id: overrides.id ?? "event-1",
|
|
name: overrides.name ?? "Test Event",
|
|
eventDate: overrides.eventDate ?? "2026-03-20",
|
|
eventStartsAt: overrides.eventStartsAt ?? null,
|
|
eventType: overrides.eventType ?? "playoff_game",
|
|
isComplete: overrides.isComplete ?? false,
|
|
completedAt: overrides.completedAt ?? null,
|
|
sportsSeasonId: overrides.sportsSeasonId ?? "season-1",
|
|
sportsSeason: overrides.sportsSeason ?? {
|
|
name: "2026 NFL Playoffs",
|
|
sport: { name: "NFL", slug: "nfl" },
|
|
},
|
|
};
|
|
}
|
|
|
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
|
|
describe("getEventsForDates", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockSelect.mockReturnValue(makeSelectChain([]));
|
|
});
|
|
|
|
it("returns empty array and skips DB when no dates given", async () => {
|
|
const result = await getEventsForDates([]);
|
|
expect(result).toEqual([]);
|
|
expect(mockSelect).not.toHaveBeenCalled();
|
|
expect(mockFindMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("returns empty array when select finds no matching event IDs", async () => {
|
|
mockSelect.mockReturnValue(makeSelectChain([]));
|
|
|
|
const result = await getEventsForDates(["2026-03-20"]);
|
|
|
|
expect(result).toEqual([]);
|
|
expect(mockFindMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("maps database rows to DashboardScoringEvent shape", async () => {
|
|
mockSelect.mockReturnValue(makeSelectChain([{ id: "e1", eventDate: "2026-03-20", gameDate: null }]));
|
|
mockFindMany.mockResolvedValueOnce([
|
|
makeEventRow({
|
|
id: "e1",
|
|
name: "Conference Finals",
|
|
eventDate: "2026-03-20",
|
|
eventType: "playoff_game",
|
|
isComplete: false,
|
|
sportsSeasonId: "ss-1",
|
|
sportsSeason: {
|
|
name: "2026 NBA Playoffs",
|
|
sport: { name: "NBA", slug: "nba" },
|
|
},
|
|
}),
|
|
]);
|
|
|
|
const result = await getEventsForDates(["2026-03-20"]);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toMatchObject({
|
|
id: "e1",
|
|
name: "Conference Finals",
|
|
eventDate: "2026-03-20",
|
|
eventType: "playoff_game",
|
|
isComplete: false,
|
|
sportsSeasonId: "ss-1",
|
|
sportsSeasonName: "2026 NBA Playoffs",
|
|
sportName: "NBA",
|
|
sportSlug: "nba",
|
|
});
|
|
});
|
|
|
|
it("returns events for multiple dates", async () => {
|
|
mockSelect.mockReturnValue(
|
|
makeSelectChain([{ id: "e1", eventDate: "2026-03-20", gameDate: null }, { id: "e2", eventDate: "2026-03-21", gameDate: null }])
|
|
);
|
|
mockFindMany.mockResolvedValueOnce([
|
|
makeEventRow({ id: "e1", eventDate: "2026-03-20" }),
|
|
makeEventRow({ id: "e2", eventDate: "2026-03-21" }),
|
|
]);
|
|
|
|
const result = await getEventsForDates(["2026-03-20", "2026-03-21"]);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result.map((e) => e.id)).toEqual(["e1", "e2"]);
|
|
});
|
|
|
|
it("includes completed events with isComplete: true", async () => {
|
|
mockSelect.mockReturnValue(makeSelectChain([{ id: "e-done", eventDate: "2026-03-20", gameDate: null }]));
|
|
mockFindMany.mockResolvedValueOnce([
|
|
makeEventRow({
|
|
id: "e-done",
|
|
isComplete: true,
|
|
completedAt: new Date("2026-03-20T18:00:00Z"),
|
|
}),
|
|
]);
|
|
|
|
const result = await getEventsForDates(["2026-03-20"]);
|
|
|
|
expect(result[0].isComplete).toBe(true);
|
|
expect(result[0].completedAt).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it("includes eventStartsAt when present", async () => {
|
|
const starts = new Date("2026-03-20T19:30:00Z");
|
|
mockSelect.mockReturnValue(makeSelectChain([{ id: "e1", eventDate: "2026-03-20", gameDate: null }]));
|
|
mockFindMany.mockResolvedValueOnce([
|
|
makeEventRow({ id: "e1", eventStartsAt: starts }),
|
|
]);
|
|
|
|
const result = await getEventsForDates(["2026-03-20"]);
|
|
|
|
expect(result[0].eventStartsAt).toEqual(starts);
|
|
});
|
|
|
|
it("uses two DB calls: select then findMany", async () => {
|
|
mockSelect.mockReturnValue(makeSelectChain([{ id: "e1", eventDate: "2026-03-20", gameDate: null }]));
|
|
mockFindMany.mockResolvedValueOnce([makeEventRow({ id: "e1" })]);
|
|
|
|
await getEventsForDates(["2026-03-20"]);
|
|
|
|
expect(mockSelect).toHaveBeenCalledOnce();
|
|
expect(mockFindMany).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("passes event IDs from step 1 into findMany in step 2", async () => {
|
|
mockSelect.mockReturnValue(
|
|
makeSelectChain([{ id: "abc", eventDate: "2026-03-20", gameDate: null }, { id: "def", eventDate: "2026-03-20", gameDate: null }])
|
|
);
|
|
mockFindMany.mockResolvedValueOnce([]);
|
|
|
|
await getEventsForDates(["2026-03-20"]);
|
|
|
|
const findManyArgs = mockFindMany.mock.calls[0][0];
|
|
const whereStr = JSON.stringify(findManyArgs.where);
|
|
expect(whereStr).toContain("abc");
|
|
expect(whereStr).toContain("def");
|
|
});
|
|
|
|
it("uses leftJoin so bracket events without playoffMatchGames still qualify via eventDate", async () => {
|
|
const chain = makeSelectChain([]);
|
|
mockSelect.mockReturnValue(chain);
|
|
|
|
await getEventsForDates(["2026-03-20"]);
|
|
|
|
expect(chain.leftJoin).toHaveBeenCalled();
|
|
});
|
|
|
|
it("includes both eventDate and scheduledAt timestamp bounds in the where clause", async () => {
|
|
const chain = makeSelectChain([]);
|
|
mockSelect.mockReturnValue(chain);
|
|
|
|
await getEventsForDates(["2026-03-20", "2026-03-21"]);
|
|
|
|
// The where clause is the first (and only) arg to chain.where
|
|
const whereArg = JSON.stringify((chain.where as ReturnType<typeof vi.fn>).mock.calls[0][0]);
|
|
// eventDate inArray check
|
|
expect(whereArg).toContain("2026-03-20");
|
|
expect(whereArg).toContain("2026-03-21");
|
|
// timestamp bounds for scheduledAt
|
|
expect(whereArg).toContain("T00:00:00.000Z");
|
|
expect(whereArg).toContain("T23:59:59.999Z");
|
|
});
|
|
|
|
it("excludes schedule_event type via the where clause", async () => {
|
|
const chain = makeSelectChain([]);
|
|
mockSelect.mockReturnValue(chain);
|
|
|
|
await getEventsForDates(["2026-03-20"]);
|
|
|
|
const whereArg = JSON.stringify((chain.where as ReturnType<typeof vi.fn>).mock.calls[0][0]);
|
|
expect(whereArg).toContain("playoff_game");
|
|
expect(whereArg).toContain("major_tournament");
|
|
expect(whereArg).toContain("final_standings");
|
|
expect(whereArg).not.toContain("schedule_event");
|
|
});
|
|
|
|
it("sets displayDate from eventDate when present", async () => {
|
|
mockSelect.mockReturnValue(makeSelectChain([{ id: "e1", eventDate: "2026-03-20", gameDate: null }]));
|
|
mockFindMany.mockResolvedValueOnce([makeEventRow({ id: "e1", eventDate: "2026-03-20" })]);
|
|
|
|
const result = await getEventsForDates(["2026-03-20"]);
|
|
|
|
expect(result[0].displayDate).toBe("2026-03-20");
|
|
});
|
|
|
|
it("sets displayDate from gameDate when eventDate is null", async () => {
|
|
mockSelect.mockReturnValue(makeSelectChain([{ id: "e1", eventDate: null, gameDate: "2026-03-20" }]));
|
|
mockFindMany.mockResolvedValueOnce([makeEventRow({ id: "e1", eventDate: null })]);
|
|
|
|
const result = await getEventsForDates(["2026-03-20"]);
|
|
|
|
expect(result[0].displayDate).toBe("2026-03-20");
|
|
});
|
|
});
|