169 lines
4.4 KiB
TypeScript
169 lines
4.4 KiB
TypeScript
|
|
import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest";
|
||
|
|
import { countSeasonRaces, hasRaceRun } from "../season-races";
|
||
|
|
|
||
|
|
vi.mock("~/database/context", () => ({
|
||
|
|
database: vi.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
const NOW = new Date("2026-08-17T12:00:00.000Z");
|
||
|
|
const TODAY = "2026-08-17";
|
||
|
|
|
||
|
|
interface EventRow {
|
||
|
|
eventType: string;
|
||
|
|
isComplete: boolean;
|
||
|
|
eventDate: string | null;
|
||
|
|
eventStartsAt: Date | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeEvent(overrides: Partial<EventRow> = {}): EventRow {
|
||
|
|
return {
|
||
|
|
eventType: "schedule_event",
|
||
|
|
isComplete: false,
|
||
|
|
eventDate: null,
|
||
|
|
eventStartsAt: null,
|
||
|
|
...overrides,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
async function mockEvents(events: EventRow[]) {
|
||
|
|
const { database } = await import("~/database/context");
|
||
|
|
(database as unknown as MockInstance).mockReturnValue({
|
||
|
|
query: {
|
||
|
|
scoringEvents: {
|
||
|
|
findMany: vi.fn().mockResolvedValue(events),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
await mockEvents([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("hasRaceRun", () => {
|
||
|
|
it("trusts isComplete when an admin has set it", () => {
|
||
|
|
expect(
|
||
|
|
hasRaceRun(
|
||
|
|
{ isComplete: true, eventDate: "2026-12-31", eventStartsAt: null },
|
||
|
|
NOW,
|
||
|
|
TODAY
|
||
|
|
)
|
||
|
|
).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("prefers eventStartsAt over eventDate", () => {
|
||
|
|
// Same day, but the green flag already dropped this morning.
|
||
|
|
expect(
|
||
|
|
hasRaceRun(
|
||
|
|
{
|
||
|
|
isComplete: false,
|
||
|
|
eventDate: TODAY,
|
||
|
|
eventStartsAt: new Date("2026-08-17T09:00:00.000Z"),
|
||
|
|
},
|
||
|
|
NOW,
|
||
|
|
TODAY
|
||
|
|
)
|
||
|
|
).toBe(true);
|
||
|
|
|
||
|
|
expect(
|
||
|
|
hasRaceRun(
|
||
|
|
{
|
||
|
|
isComplete: false,
|
||
|
|
eventDate: TODAY,
|
||
|
|
eventStartsAt: new Date("2026-08-17T18:00:00.000Z"),
|
||
|
|
},
|
||
|
|
NOW,
|
||
|
|
TODAY
|
||
|
|
)
|
||
|
|
).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("treats a past date as run even when nobody marked it complete", () => {
|
||
|
|
expect(
|
||
|
|
hasRaceRun(
|
||
|
|
{ isComplete: false, eventDate: "2026-08-16", eventStartsAt: null },
|
||
|
|
NOW,
|
||
|
|
TODAY
|
||
|
|
)
|
||
|
|
).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("treats a race happening today as still upcoming", () => {
|
||
|
|
expect(
|
||
|
|
hasRaceRun({ isComplete: false, eventDate: TODAY, eventStartsAt: null }, NOW, TODAY)
|
||
|
|
).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("treats an undated row as upcoming", () => {
|
||
|
|
expect(
|
||
|
|
hasRaceRun({ isComplete: false, eventDate: null, eventStartsAt: null }, NOW, TODAY)
|
||
|
|
).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("countSeasonRaces", () => {
|
||
|
|
it("counts schedule_event rows as races", async () => {
|
||
|
|
// This is the whole bug: a season_standings calendar is stored as
|
||
|
|
// schedule_event rows, and the simulator used to skip them.
|
||
|
|
await mockEvents([
|
||
|
|
makeEvent({ eventDate: "2026-03-01" }),
|
||
|
|
makeEvent({ eventDate: "2026-04-01" }),
|
||
|
|
makeEvent({ eventDate: "2026-09-01" }),
|
||
|
|
]);
|
||
|
|
expect(await countSeasonRaces("s1", NOW)).toEqual({
|
||
|
|
completed: 2,
|
||
|
|
remaining: 1,
|
||
|
|
total: 3,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("excludes the final_standings scoring row", async () => {
|
||
|
|
await mockEvents([
|
||
|
|
makeEvent({ eventDate: "2026-03-01" }),
|
||
|
|
makeEvent({ eventType: "final_standings", eventDate: "2026-11-01" }),
|
||
|
|
]);
|
||
|
|
expect(await countSeasonRaces("s1", NOW)).toEqual({
|
||
|
|
completed: 1,
|
||
|
|
remaining: 0,
|
||
|
|
total: 1,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("counts other event types too, whichever type the admin used", async () => {
|
||
|
|
await mockEvents([
|
||
|
|
makeEvent({ eventType: "major_tournament", eventDate: "2026-03-01" }),
|
||
|
|
makeEvent({ eventType: "playoff_game", eventDate: "2026-09-01" }),
|
||
|
|
]);
|
||
|
|
expect(await countSeasonRaces("s1", NOW)).toEqual({
|
||
|
|
completed: 1,
|
||
|
|
remaining: 1,
|
||
|
|
total: 2,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("returns zeroes when the season has no events", async () => {
|
||
|
|
expect(await countSeasonRaces("s1", NOW)).toEqual({
|
||
|
|
completed: 0,
|
||
|
|
remaining: 0,
|
||
|
|
total: 0,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("counts a realistic late-season IndyCar calendar", async () => {
|
||
|
|
const calendar = [
|
||
|
|
...Array.from({ length: 15 }, (_, i) =>
|
||
|
|
makeEvent({ eventDate: `2026-0${((i % 6) + 3)}-0${(i % 9) + 1}` })
|
||
|
|
),
|
||
|
|
makeEvent({ eventStartsAt: new Date("2026-08-30T18:00:00.000Z") }),
|
||
|
|
makeEvent({ eventStartsAt: new Date("2026-09-13T18:00:00.000Z") }),
|
||
|
|
makeEvent({ eventType: "final_standings" }),
|
||
|
|
];
|
||
|
|
await mockEvents(calendar);
|
||
|
|
expect(await countSeasonRaces("s1", NOW)).toEqual({
|
||
|
|
completed: 15,
|
||
|
|
remaining: 2,
|
||
|
|
total: 17,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|