2026-03-12 10:12:38 -07:00
|
|
|
/**
|
|
|
|
|
* Tests for getUpcomingEventsForDraftedParticipants logic.
|
|
|
|
|
*
|
|
|
|
|
* We test the exported function indirectly by exercising the pure
|
|
|
|
|
* business-logic paths (all-compete vs bracket, date filtering,
|
|
|
|
|
* deduplication, empty inputs) using a vi.mock of the database context
|
|
|
|
|
* so no real DB is required.
|
|
|
|
|
*/
|
|
|
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
|
|
|
|
|
|
// ── DB mock ────────────────────────────────────────────────────────────────
|
|
|
|
|
// Must be declared before the import that triggers the module graph.
|
|
|
|
|
const mockDb = {
|
|
|
|
|
query: {
|
|
|
|
|
scoringEvents: { findMany: vi.fn() },
|
|
|
|
|
},
|
|
|
|
|
selectDistinct: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
vi.mock("~/database/context", () => ({
|
|
|
|
|
database: () => mockDb,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("~/database/schema", () => ({
|
|
|
|
|
scoringEvents: { sportsSeasonId: "se.sports_season_id", isComplete: "se.is_complete", eventDate: "se.event_date", eventType: "se.event_type" },
|
|
|
|
|
playoffMatches: { scoringEventId: "pm.scoring_event_id", participant1Id: "pm.participant1_id", participant2Id: "pm.participant2_id", round: "pm.round", matchNumber: "pm.match_number" },
|
|
|
|
|
playoffMatchGames: { playoffMatchId: "pmg.playoff_match_id", scheduledAt: "pmg.scheduled_at", gameNumber: "pmg.game_number" },
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
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 }),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
import { getUpcomingEventsForDraftedParticipants } from "../scoring-event";
|
|
|
|
|
|
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
|
const DATE_FROM = new Date("2025-04-01");
|
|
|
|
|
const DATE_TO = new Date("2025-04-30");
|
|
|
|
|
const SPORTS_SEASON_ID = "ss-1";
|
|
|
|
|
|
|
|
|
|
function makeParticipant(id: string, name: string) {
|
|
|
|
|
return { id, name };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function makeScoringEvent(overrides: Partial<{
|
|
|
|
|
id: string; name: string; eventDate: string | null;
|
2026-03-15 10:22:42 -07:00
|
|
|
eventStartsAt: Date | null;
|
2026-03-12 10:12:38 -07:00
|
|
|
eventType: string; sportsSeasonId: string; isComplete: boolean;
|
|
|
|
|
}> = {}) {
|
|
|
|
|
return {
|
|
|
|
|
id: overrides.id ?? "event-1",
|
|
|
|
|
name: overrides.name ?? "Test Event",
|
|
|
|
|
eventDate: overrides.eventDate ?? "2025-04-10",
|
2026-03-15 10:22:42 -07:00
|
|
|
eventStartsAt: overrides.eventStartsAt ?? null,
|
2026-03-12 10:12:38 -07:00
|
|
|
eventType: overrides.eventType ?? "schedule_event",
|
|
|
|
|
sportsSeasonId: overrides.sportsSeasonId ?? SPORTS_SEASON_ID,
|
|
|
|
|
isComplete: overrides.isComplete ?? false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── All-compete patterns ───────────────────────────────────────────────────
|
|
|
|
|
describe("getUpcomingEventsForDraftedParticipants — all-compete sports", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
// selectDistinct won't be called for all-compete
|
|
|
|
|
mockDb.selectDistinct.mockReturnValue({ from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockResolvedValue([]) });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns [] when draftedParticipants is empty", async () => {
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "season_standings", [], DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
expect(result).toEqual([]);
|
|
|
|
|
expect(mockDb.query.scoringEvents.findMany).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns all events with all drafted participants attached", async () => {
|
|
|
|
|
const participants = [makeParticipant("p1", "Verstappen"), makeParticipant("p2", "Hamilton")];
|
|
|
|
|
const events = [makeScoringEvent({ id: "e1", name: "Australian GP" })];
|
|
|
|
|
mockDb.query.scoringEvents.findMany.mockResolvedValue(events);
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result).toHaveLength(1);
|
|
|
|
|
expect(result[0].name).toBe("Australian GP");
|
|
|
|
|
expect(result[0].relevantParticipants).toEqual(participants);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("works for qualifying_points pattern", async () => {
|
|
|
|
|
const participants = [makeParticipant("g1", "Scheffler")];
|
|
|
|
|
mockDb.query.scoringEvents.findMany.mockResolvedValue([makeScoringEvent({ id: "e1", name: "Masters" })]);
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "qualifying_points", participants, DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
expect(result).toHaveLength(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("unknown pattern (e.g. golf_qualifying_points) falls through to bracket path, not all-compete", async () => {
|
|
|
|
|
// golf_qualifying_points is a simulatorType, not a scoringPattern.
|
|
|
|
|
// Unknown patterns are treated as bracket sports (selectDistinct path).
|
|
|
|
|
const participants = [makeParticipant("g1", "McIlroy")];
|
|
|
|
|
const selectChain = { from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), leftJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockResolvedValue([]) };
|
|
|
|
|
mockDb.selectDistinct.mockReturnValue(selectChain);
|
|
|
|
|
|
|
|
|
|
await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "golf_qualifying_points", participants, DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Should NOT call the all-compete findMany path
|
|
|
|
|
expect(mockDb.query.scoringEvents.findMany).not.toHaveBeenCalled();
|
|
|
|
|
// Should call the bracket selectDistinct path
|
|
|
|
|
expect(mockDb.selectDistinct).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns multiple events sorted by eventDate", async () => {
|
|
|
|
|
const participants = [makeParticipant("p1", "Verstappen")];
|
|
|
|
|
const events = [
|
|
|
|
|
makeScoringEvent({ id: "e2", name: "Miami GP", eventDate: "2025-04-20" }),
|
|
|
|
|
makeScoringEvent({ id: "e1", name: "Australian GP", eventDate: "2025-04-06" }),
|
|
|
|
|
];
|
|
|
|
|
// findMany is ordered by asc(eventDate) in the real query; simulate same order
|
|
|
|
|
mockDb.query.scoringEvents.findMany.mockResolvedValue([events[1], events[0]]);
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
expect(result[0].name).toBe("Australian GP");
|
|
|
|
|
expect(result[1].name).toBe("Miami GP");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Bracket patterns ───────────────────────────────────────────────────────
|
|
|
|
|
describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
|
|
|
|
function makeSelectChain(rows: unknown[]) {
|
|
|
|
|
const chain = {
|
|
|
|
|
from: vi.fn().mockReturnThis(),
|
|
|
|
|
innerJoin: vi.fn().mockReturnThis(),
|
|
|
|
|
leftJoin: vi.fn().mockReturnThis(),
|
|
|
|
|
where: vi.fn().mockReturnThis(),
|
|
|
|
|
orderBy: vi.fn().mockResolvedValue(rows),
|
|
|
|
|
};
|
|
|
|
|
return chain;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns [] when draftedParticipants is empty", async () => {
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "playoff_bracket", [], DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
expect(result).toEqual([]);
|
|
|
|
|
expect(mockDb.selectDistinct).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns one event with one relevant participant (participant1)", async () => {
|
|
|
|
|
const rows = [{
|
|
|
|
|
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
|
|
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
|
|
|
participant1Id: "man-city", participant2Id: "bayern",
|
|
|
|
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
|
|
|
|
}];
|
|
|
|
|
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
|
|
|
[makeParticipant("man-city", "Man City")],
|
|
|
|
|
DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result).toHaveLength(1);
|
|
|
|
|
expect(result[0].name).toBe("UCL QF");
|
|
|
|
|
expect(result[0].relevantParticipants).toEqual([{ id: "man-city", name: "Man City" }]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns one event when both participants are drafted (no duplication)", async () => {
|
|
|
|
|
// Two rows for the same event — the DB query uses selectDistinct on event columns
|
|
|
|
|
// but we may still get two rows if the WHERE matches on both participants.
|
|
|
|
|
// The grouping logic must deduplicate the event itself.
|
|
|
|
|
const rows = [
|
|
|
|
|
{
|
|
|
|
|
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
|
|
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
|
|
|
participant1Id: "man-city", participant2Id: "arsenal",
|
|
|
|
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
|
|
|
[makeParticipant("man-city", "Man City"), makeParticipant("arsenal", "Arsenal")],
|
|
|
|
|
DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result).toHaveLength(1);
|
|
|
|
|
expect(result[0].relevantParticipants).toHaveLength(2);
|
|
|
|
|
const names = result[0].relevantParticipants.map((p) => p.name);
|
|
|
|
|
expect(names).toContain("Man City");
|
|
|
|
|
expect(names).toContain("Arsenal");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns multiple events when picks are in different matches", async () => {
|
|
|
|
|
const rows = [
|
|
|
|
|
{
|
|
|
|
|
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
|
|
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
|
|
|
participant1Id: "man-city", participant2Id: "real-madrid",
|
|
|
|
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
|
|
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
|
|
|
participant1Id: "arsenal", participant2Id: "psg",
|
|
|
|
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
|
|
|
[makeParticipant("man-city", "Man City"), makeParticipant("arsenal", "Arsenal")],
|
|
|
|
|
DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Both matches are in the same event → one result with both picks
|
|
|
|
|
expect(result).toHaveLength(1);
|
|
|
|
|
expect(result[0].relevantParticipants).toHaveLength(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("only includes drafted participants in relevantParticipants, not opponents", async () => {
|
|
|
|
|
const rows = [{
|
|
|
|
|
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
|
|
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
|
|
|
participant1Id: "man-city", participant2Id: "real-madrid",
|
|
|
|
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
|
|
|
|
}];
|
|
|
|
|
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
|
|
|
[makeParticipant("man-city", "Man City")],
|
|
|
|
|
DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result[0].relevantParticipants).toEqual([{ id: "man-city", name: "Man City" }]);
|
|
|
|
|
// Real Madrid (the opponent) should NOT appear
|
|
|
|
|
expect(result[0].relevantParticipants.find((p) => p.id === "real-madrid")).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("works for ucl_bracket pattern", async () => {
|
|
|
|
|
mockDb.selectDistinct.mockReturnValue(makeSelectChain([{
|
|
|
|
|
id: "e1", name: "UCL SF", eventDate: "2025-04-29",
|
|
|
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
|
|
|
participant1Id: "man-city", participant2Id: "arsenal",
|
|
|
|
|
round: "Semifinals", gameNumber: 1, scheduledAt: null,
|
|
|
|
|
}]));
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "ucl_bracket",
|
|
|
|
|
[makeParticipant("man-city", "Man City")],
|
|
|
|
|
DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
expect(result).toHaveLength(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("includes events with null eventDate when they have games scheduled within the window", async () => {
|
|
|
|
|
// Real-world case: admin sets game times on individual games (scheduledAt)
|
|
|
|
|
// but leaves scoringEvent.eventDate null — the event must still appear.
|
|
|
|
|
const rows = [{
|
|
|
|
|
id: "e1", name: "UCL QF",
|
|
|
|
|
eventDate: null, // ← no date on the event itself
|
|
|
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
|
|
|
participant1Id: "barcelona", participant2Id: "sporting",
|
|
|
|
|
round: "Knockout Stage", gameNumber: 1, scheduledAt: null,
|
|
|
|
|
}];
|
|
|
|
|
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
|
|
|
[makeParticipant("barcelona", "Barcelona")],
|
|
|
|
|
DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result).toHaveLength(1);
|
|
|
|
|
expect(result[0].name).toBe("UCL QF");
|
|
|
|
|
expect(result[0].eventDate).toBeNull();
|
|
|
|
|
expect(result[0].relevantParticipants).toEqual([{ id: "barcelona", name: "Barcelona" }]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("surfaces earliestGameTime from scheduledAt when eventDate is null", async () => {
|
|
|
|
|
const gameTime = new Date("2025-04-09T14:00:00.000Z");
|
|
|
|
|
const rows = [{
|
|
|
|
|
id: "e1", name: "UCL QF",
|
|
|
|
|
eventDate: null,
|
|
|
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
|
|
|
participant1Id: "barcelona", participant2Id: "sporting",
|
|
|
|
|
round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime,
|
|
|
|
|
}];
|
|
|
|
|
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
|
|
|
[makeParticipant("barcelona", "Barcelona")],
|
|
|
|
|
DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result[0].earliestGameTime).toBe(gameTime.toISOString());
|
|
|
|
|
expect(result[0].eventDate).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("picks the earliest scheduledAt when multiple game rows exist for an event", async () => {
|
|
|
|
|
const gameTime1 = new Date("2025-04-09T13:00:00.000Z");
|
|
|
|
|
const gameTime2 = new Date("2025-04-09T15:00:00.000Z");
|
|
|
|
|
const rows = [
|
|
|
|
|
{
|
|
|
|
|
id: "e1", name: "UCL QF", eventDate: null,
|
|
|
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
|
|
|
participant1Id: "barcelona", participant2Id: "sporting",
|
|
|
|
|
round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime2,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "e1", name: "UCL QF", eventDate: null,
|
|
|
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
|
|
|
participant1Id: "barcelona", participant2Id: "sporting",
|
|
|
|
|
round: "Knockout Stage", gameNumber: 2, scheduledAt: gameTime1,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
|
|
|
[makeParticipant("barcelona", "Barcelona")],
|
|
|
|
|
DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Should use the earlier time
|
|
|
|
|
expect(result[0].earliestGameTime).toBe(gameTime1.toISOString());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("sets matchLabel to round+game when single game key (non-redundant)", async () => {
|
|
|
|
|
const rows = [{
|
|
|
|
|
id: "e1", name: "PDC World Championship", eventDate: "2025-04-09",
|
|
|
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
|
|
|
participant1Id: "man-city", participant2Id: "arsenal",
|
|
|
|
|
round: "Semifinals", gameNumber: 1, scheduledAt: null,
|
|
|
|
|
}];
|
|
|
|
|
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
|
|
|
[makeParticipant("man-city", "Man City")],
|
|
|
|
|
DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result[0].matchLabel).toBe("Semifinals Game #1");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("omits redundant round prefix when round name matches event name", async () => {
|
|
|
|
|
// e.g. event named "Knockout Stage", round also "Knockout Stage", single game
|
|
|
|
|
// → show "Game #1" rather than the redundant "Knockout Stage Game #1"
|
|
|
|
|
const rows = [{
|
|
|
|
|
id: "e1", name: "Knockout Stage", eventDate: null,
|
|
|
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
|
|
|
participant1Id: "barcelona", participant2Id: "sporting",
|
|
|
|
|
round: "Knockout Stage", gameNumber: 1, scheduledAt: null,
|
|
|
|
|
}];
|
|
|
|
|
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
|
|
|
[makeParticipant("barcelona", "Barcelona")],
|
|
|
|
|
DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result[0].matchLabel).toBe("Game #1");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("all-compete events have null earliestGameTime and matchLabel", async () => {
|
|
|
|
|
const participants = [makeParticipant("p1", "Verstappen")];
|
|
|
|
|
mockDb.query.scoringEvents.findMany.mockResolvedValue([
|
|
|
|
|
makeScoringEvent({ id: "e1", name: "Australian GP" }),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result[0].earliestGameTime).toBeNull();
|
|
|
|
|
expect(result[0].matchLabel).toBeNull();
|
|
|
|
|
});
|
2026-03-15 10:22:42 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── eventStartsAt — all-compete sports ────────────────────────────────────
|
|
|
|
|
describe("getUpcomingEventsForDraftedParticipants — eventStartsAt for all-compete sports", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
mockDb.selectDistinct.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnThis(),
|
|
|
|
|
innerJoin: vi.fn().mockReturnThis(),
|
|
|
|
|
where: vi.fn().mockReturnThis(),
|
|
|
|
|
orderBy: vi.fn().mockResolvedValue([]),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("populates earliestGameTime from eventStartsAt when set", async () => {
|
|
|
|
|
const raceStart = new Date("2025-04-06T14:00:00.000Z");
|
|
|
|
|
const participants = [makeParticipant("p1", "Verstappen")];
|
|
|
|
|
mockDb.query.scoringEvents.findMany.mockResolvedValue([
|
|
|
|
|
makeScoringEvent({ id: "e1", name: "Australian GP", eventStartsAt: raceStart }),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result[0].earliestGameTime).toBe(raceStart.toISOString());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("leaves earliestGameTime null when eventStartsAt is not set", async () => {
|
|
|
|
|
const participants = [makeParticipant("p1", "Verstappen")];
|
|
|
|
|
mockDb.query.scoringEvents.findMany.mockResolvedValue([
|
|
|
|
|
makeScoringEvent({ id: "e1", name: "Australian GP", eventStartsAt: null }),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result[0].earliestGameTime).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("works for qualifying_points pattern with eventStartsAt", async () => {
|
|
|
|
|
const majorStart = new Date("2025-04-10T12:00:00.000Z");
|
|
|
|
|
const participants = [makeParticipant("g1", "Scheffler")];
|
|
|
|
|
mockDb.query.scoringEvents.findMany.mockResolvedValue([
|
|
|
|
|
makeScoringEvent({ id: "e1", name: "The Masters", eventStartsAt: majorStart }),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "qualifying_points", participants, DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result[0].earliestGameTime).toBe(majorStart.toISOString());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("earliestGameTime from eventStartsAt is used as sort key over eventDate", async () => {
|
|
|
|
|
// Event on Apr 6, game starts at 14:00 UTC — both should be reflected in earliestGameTime
|
|
|
|
|
const raceStart = new Date("2025-04-06T14:00:00.000Z");
|
|
|
|
|
const participants = [makeParticipant("p1", "Hamilton")];
|
|
|
|
|
mockDb.query.scoringEvents.findMany.mockResolvedValue([
|
|
|
|
|
makeScoringEvent({ id: "e1", name: "Bahrain GP", eventDate: "2025-04-06", eventStartsAt: raceStart }),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// earliestGameTime carries the full timestamp so the sort key has time precision
|
|
|
|
|
expect(result[0].earliestGameTime).toBe("2025-04-06T14:00:00.000Z");
|
|
|
|
|
expect(result[0].eventDate).toBe("2025-04-06");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Bracket: misc ──────────────────────────────────────────────────────────
|
|
|
|
|
describe("getUpcomingEventsForDraftedParticipants — bracket misc", () => {
|
|
|
|
|
function makeSelectChain(rows: unknown[]) {
|
|
|
|
|
return {
|
|
|
|
|
from: vi.fn().mockReturnThis(),
|
|
|
|
|
innerJoin: vi.fn().mockReturnThis(),
|
|
|
|
|
leftJoin: vi.fn().mockReturnThis(),
|
|
|
|
|
where: vi.fn().mockReturnThis(),
|
|
|
|
|
orderBy: vi.fn().mockResolvedValue(rows),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
beforeEach(() => vi.clearAllMocks());
|
2026-03-12 10:12:38 -07:00
|
|
|
|
|
|
|
|
it("uses leftJoin so events without any games are still considered", async () => {
|
|
|
|
|
const chain = makeSelectChain([]);
|
|
|
|
|
mockDb.selectDistinct.mockReturnValue(chain);
|
|
|
|
|
|
|
|
|
|
await getUpcomingEventsForDraftedParticipants(
|
|
|
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
|
|
|
[makeParticipant("man-city", "Man City")],
|
|
|
|
|
DATE_FROM, DATE_TO
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(chain.leftJoin).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|