brackt/app/models/__tests__/season-races.test.ts

193 lines
5.4 KiB
TypeScript
Raw Permalink Normal View History

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", () => {
// Started yesterday and long finished, even though eventDate is unset.
expect(
hasRaceRun(
{
isComplete: false,
eventDate: null,
eventStartsAt: new Date("2026-08-16T18: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("does not call a race run the moment it goes green", () => {
// Declaring the finale finished at the green flag would publish the
// pre-race leader as champion at 100%, from standings without that race.
const greenFlag = new Date(NOW.getTime() - 30 * 60 * 1000);
expect(
hasRaceRun({ isComplete: false, eventDate: TODAY, eventStartsAt: greenFlag }, NOW, TODAY)
).toBe(false);
});
it("counts a race run once it has had time to finish", () => {
const greenFlag = new Date(NOW.getTime() - 7 * 60 * 60 * 1000);
expect(
hasRaceRun({ isComplete: false, eventDate: TODAY, eventStartsAt: greenFlag }, NOW, TODAY)
).toBe(true);
});
it("still honours isComplete for a race that just started", () => {
const greenFlag = new Date(NOW.getTime() - 30 * 60 * 1000);
expect(
hasRaceRun({ isComplete: true, eventDate: TODAY, eventStartsAt: greenFlag }, NOW, TODAY)
).toBe(true);
});
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}` })
),
// The next race goes green in a few hours — still remaining.
makeEvent({ eventStartsAt: new Date("2026-08-17T18:00:00.000Z") }),
makeEvent({ eventStartsAt: new Date("2026-08-30T18:00:00.000Z") }),
makeEvent({ eventType: "final_standings" }),
];
await mockEvents(calendar);
expect(await countSeasonRaces("s1", NOW)).toEqual({
completed: 15,
remaining: 2,
total: 17,
});
});
});