Include playoffMatchId in the grouping key so that each match within a scoring event (e.g. different Round of 32 matchups in snooker) appears as its own calendar entry instead of being merged together.
664 lines
28 KiB
TypeScript
664 lines
28 KiB
TypeScript
/**
|
|
* 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: { id: "pm.id", scoringEventId: "pm.scoring_event_id", participant1Id: "pm.participant1_id", participant2Id: "pm.participant2_id", round: "pm.round", matchNumber: "pm.match_number", isComplete: "pm.is_complete" },
|
|
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 = "2025-04-01";
|
|
const DATE_TO = "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;
|
|
eventStartsAt: Date | null;
|
|
eventType: string; sportsSeasonId: string; isComplete: boolean;
|
|
}> = {}) {
|
|
return {
|
|
id: overrides.id ?? "event-1",
|
|
name: overrides.name ?? "Test Event",
|
|
eventDate: overrides.eventDate ?? "2025-04-10",
|
|
eventStartsAt: overrides.eventStartsAt ?? null,
|
|
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", () => {
|
|
// Main query chain: resolves on orderBy (has leftJoin, orderBy)
|
|
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;
|
|
}
|
|
|
|
// Second query chain (max game lookup): resolves on where (no leftJoin, no orderBy)
|
|
function makeMaxGameChain(rows: unknown[]) {
|
|
const chain = {
|
|
from: vi.fn().mockReturnThis(),
|
|
innerJoin: vi.fn().mockReturnThis(),
|
|
where: vi.fn().mockResolvedValue(rows),
|
|
};
|
|
return chain;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Default for the second selectDistinct call (max-game-number lookup).
|
|
// Individual tests override the first call with mockReturnValueOnce.
|
|
mockDb.selectDistinct.mockReturnValue(makeMaxGameChain([]));
|
|
});
|
|
|
|
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,
|
|
playoffMatchId: "pm1",
|
|
participant1Id: "man-city", participant2Id: "bayern",
|
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
|
}];
|
|
mockDb.selectDistinct.mockReturnValueOnce(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,
|
|
playoffMatchId: "pm1",
|
|
participant1Id: "man-city", participant2Id: "arsenal",
|
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
|
},
|
|
];
|
|
mockDb.selectDistinct.mockReturnValueOnce(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 separate events when picks are in different matches within the same scoring event", async () => {
|
|
const rows = [
|
|
{
|
|
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
playoffMatchId: "pm1",
|
|
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,
|
|
playoffMatchId: "pm2",
|
|
participant1Id: "arsenal", participant2Id: "psg",
|
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
|
},
|
|
];
|
|
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
[makeParticipant("man-city", "Man City"), makeParticipant("arsenal", "Arsenal")],
|
|
DATE_FROM, DATE_TO
|
|
);
|
|
|
|
// Each match is now a separate entry
|
|
expect(result).toHaveLength(2);
|
|
const manCityEntry = result.find((r) => r.relevantParticipants.some((p) => p.id === "man-city"));
|
|
const arsenalEntry = result.find((r) => r.relevantParticipants.some((p) => p.id === "arsenal"));
|
|
expect(manCityEntry).toBeDefined();
|
|
expect(arsenalEntry).toBeDefined();
|
|
expect(manCityEntry?.relevantParticipants).toHaveLength(1);
|
|
expect(arsenalEntry?.relevantParticipants).toHaveLength(1);
|
|
});
|
|
|
|
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,
|
|
playoffMatchId: "pm1",
|
|
participant1Id: "man-city", participant2Id: "real-madrid",
|
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
|
}];
|
|
mockDb.selectDistinct.mockReturnValueOnce(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.mockReturnValueOnce(makeSelectChain([{
|
|
id: "e1", name: "UCL SF", eventDate: "2025-04-29",
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
playoffMatchId: "pm1",
|
|
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,
|
|
playoffMatchId: "pm1",
|
|
participant1Id: "barcelona", participant2Id: "sporting",
|
|
round: "Knockout Stage", gameNumber: 1, scheduledAt: null,
|
|
}];
|
|
mockDb.selectDistinct.mockReturnValueOnce(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,
|
|
playoffMatchId: "pm1",
|
|
participant1Id: "barcelona", participant2Id: "sporting",
|
|
round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime,
|
|
}];
|
|
mockDb.selectDistinct.mockReturnValueOnce(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("series with both games in window produces two entries, each with correct time and label", async () => {
|
|
const gameTime1 = new Date("2025-04-09T13:00:00.000Z");
|
|
const gameTime2 = new Date("2025-04-11T15:00:00.000Z");
|
|
const rows = [
|
|
{
|
|
id: "e1", name: "UCL QF", eventDate: null,
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
playoffMatchId: "pm1",
|
|
participant1Id: "barcelona", participant2Id: "sporting",
|
|
round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime1,
|
|
},
|
|
{
|
|
id: "e1", name: "UCL QF", eventDate: null,
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
playoffMatchId: "pm1",
|
|
participant1Id: "barcelona", participant2Id: "sporting",
|
|
round: "Knockout Stage", gameNumber: 2, scheduledAt: gameTime2,
|
|
},
|
|
];
|
|
mockDb.selectDistinct
|
|
.mockReturnValueOnce(makeSelectChain(rows))
|
|
.mockReturnValueOnce(makeMaxGameChain([
|
|
{ eventId: "e1", gameNumber: 1 },
|
|
{ eventId: "e1", gameNumber: 2 },
|
|
]));
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
[makeParticipant("barcelona", "Barcelona")],
|
|
DATE_FROM, DATE_TO
|
|
);
|
|
|
|
expect(result).toHaveLength(2);
|
|
const game1 = result.find((r) => r.id === "e1|pm1|1");
|
|
const game2 = result.find((r) => r.id === "e1|pm1|2");
|
|
expect(game1?.earliestGameTime).toBe(gameTime1.toISOString());
|
|
expect(game1?.matchLabel).toBe("Knockout Stage Game #1");
|
|
expect(game2?.earliestGameTime).toBe(gameTime2.toISOString());
|
|
expect(game2?.matchLabel).toBe("Knockout Stage Game #2");
|
|
});
|
|
|
|
it("sets matchLabel to round name only (no game number) for single-game matchup", async () => {
|
|
const rows = [{
|
|
id: "e1", name: "PDC World Championship", eventDate: "2025-04-09",
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
playoffMatchId: "pm1",
|
|
participant1Id: "man-city", participant2Id: "arsenal",
|
|
round: "Semifinals", gameNumber: 1, scheduledAt: null,
|
|
}];
|
|
mockDb.selectDistinct.mockReturnValueOnce(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");
|
|
});
|
|
|
|
it("sets matchLabel to null when round name matches event name (single game)", async () => {
|
|
// e.g. event named "Knockout Stage", round also "Knockout Stage", single game
|
|
// → show null rather than redundant label
|
|
const rows = [{
|
|
id: "e1", name: "Knockout Stage", eventDate: null,
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
playoffMatchId: "pm1",
|
|
participant1Id: "barcelona", participant2Id: "sporting",
|
|
round: "Knockout Stage", gameNumber: 1, scheduledAt: null,
|
|
}];
|
|
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
[makeParticipant("barcelona", "Barcelona")],
|
|
DATE_FROM, DATE_TO
|
|
);
|
|
|
|
expect(result[0].matchLabel).toBeNull();
|
|
});
|
|
|
|
it("shows game number when only game #2 is in the window (game #1 already played)", async () => {
|
|
const rows = [{
|
|
id: "e1", name: "PDC World Championship", eventDate: "2025-04-10",
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
playoffMatchId: "pm1",
|
|
participant1Id: "man-city", participant2Id: "arsenal",
|
|
round: "Semifinals", gameNumber: 2, scheduledAt: null,
|
|
}];
|
|
mockDb.selectDistinct
|
|
.mockReturnValueOnce(makeSelectChain(rows))
|
|
.mockReturnValueOnce(makeMaxGameChain([
|
|
{ eventId: "e1", gameNumber: 1 },
|
|
{ eventId: "e1", gameNumber: 2 },
|
|
]));
|
|
|
|
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 #2");
|
|
});
|
|
|
|
it("shows Game #1 label when game #1 is upcoming but series has multiple games", async () => {
|
|
// Game #2 exists in DB but is outside the date window — only Game #1 appears
|
|
// in the main query. The second (un-date-filtered) query reveals it's a series.
|
|
const mainRows = [{
|
|
id: "e1", name: "NBA Finals", eventDate: "2025-04-10",
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
playoffMatchId: "pm1",
|
|
participant1Id: "lakers", participant2Id: "celtics",
|
|
round: "Finals", gameNumber: 1, scheduledAt: null,
|
|
}];
|
|
const allGameRows = [
|
|
{ eventId: "e1", gameNumber: 1 },
|
|
{ eventId: "e1", gameNumber: 2 },
|
|
];
|
|
mockDb.selectDistinct
|
|
.mockReturnValueOnce(makeSelectChain(mainRows))
|
|
.mockReturnValueOnce(makeMaxGameChain(allGameRows));
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
[makeParticipant("lakers", "Lakers")],
|
|
DATE_FROM, DATE_TO
|
|
);
|
|
|
|
expect(result[0].matchLabel).toBe("Finals Game #1");
|
|
});
|
|
|
|
it("separates matches within the same scoring event by scheduled time (snooker-style bracket)", async () => {
|
|
const time1 = new Date("2025-04-18T06:30:00.000Z");
|
|
const time2 = new Date("2025-04-19T06:30:00.000Z");
|
|
const rows = [
|
|
{
|
|
id: "e1", name: "Snooker World Championship", eventDate: null,
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
playoffMatchId: "pm-williams",
|
|
participant1Id: "mark-williams", participant2Id: "opponent-a",
|
|
round: "Round of 32", gameNumber: 1, scheduledAt: time1,
|
|
},
|
|
{
|
|
id: "e1", name: "Snooker World Championship", eventDate: null,
|
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
|
playoffMatchId: "pm-higgins",
|
|
participant1Id: "john-higgins", participant2Id: "opponent-b",
|
|
round: "Round of 32", gameNumber: 1, scheduledAt: time2,
|
|
},
|
|
];
|
|
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
[makeParticipant("mark-williams", "Mark Williams"), makeParticipant("john-higgins", "John Higgins")],
|
|
DATE_FROM, DATE_TO
|
|
);
|
|
|
|
expect(result).toHaveLength(2);
|
|
const williamsEntry = result.find((r) => r.relevantParticipants.some((p) => p.id === "mark-williams"));
|
|
const higginsEntry = result.find((r) => r.relevantParticipants.some((p) => p.id === "john-higgins"));
|
|
expect(williamsEntry).toBeDefined();
|
|
expect(higginsEntry).toBeDefined();
|
|
expect(williamsEntry?.earliestGameTime).toBe(time1.toISOString());
|
|
expect(higginsEntry?.earliestGameTime).toBe(time2.toISOString());
|
|
expect(williamsEntry?.matchLabel).toBe("Round of 32");
|
|
expect(higginsEntry?.matchLabel).toBe("Round of 32");
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|
|
|
|
// ── 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),
|
|
};
|
|
}
|
|
|
|
function makeMaxGameChain(rows: unknown[]) {
|
|
return {
|
|
from: vi.fn().mockReturnThis(),
|
|
innerJoin: vi.fn().mockReturnThis(),
|
|
where: vi.fn().mockResolvedValue(rows),
|
|
};
|
|
}
|
|
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it("excludes scored (isComplete) matches by passing the filter to the DB query", async () => {
|
|
// The DB mock returns rows only for matches the WHERE clause matches.
|
|
// When a match is complete (isComplete=true) the DB should return no rows.
|
|
// Simulate this by returning an empty result set.
|
|
// No second mockReturnValueOnce is needed: empty rows means eventIdsFromRows
|
|
// is empty so the max-game-number lookup is never called.
|
|
const chain = makeSelectChain([]);
|
|
mockDb.selectDistinct.mockReturnValueOnce(chain);
|
|
|
|
const result = await getUpcomingEventsForDraftedParticipants(
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
[makeParticipant("man-city", "Man City")],
|
|
DATE_FROM, DATE_TO
|
|
);
|
|
|
|
expect(result).toHaveLength(0);
|
|
// The WHERE clause includes eq(schema.playoffMatches.isComplete, false).
|
|
// We verify the query was built without error (accessing the field on the mock).
|
|
expect(chain.where).toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses leftJoin so events without any games are still considered", async () => {
|
|
const chain = makeSelectChain([]);
|
|
mockDb.selectDistinct
|
|
.mockReturnValueOnce(chain)
|
|
.mockReturnValueOnce(makeMaxGameChain([]));
|
|
|
|
await getUpcomingEventsForDraftedParticipants(
|
|
SPORTS_SEASON_ID, "playoff_bracket",
|
|
[makeParticipant("man-city", "Man City")],
|
|
DATE_FROM, DATE_TO
|
|
);
|
|
|
|
expect(chain.leftJoin).toHaveBeenCalled();
|
|
});
|
|
});
|