brackt/app/services/__tests__/discord.test.ts
Chris Parsons beab6400b2
Fix partial bracket scoring, rank labels, and Discord notification bugs (#158)
- NCAAM R64/R32 winners no longer receive T5 floor points; only Sweet
  Sixteen winners entering the first scoring round get a floor
- AFL T5-T6 and T7-T8 score as separate tiers (avg([5,6]) and avg([7,8]))
  instead of one shared avg([5-8]) pool
- Eliminated teams now always show their correct final rank (e.g. T33 for
  NCAAM R64 losers) even during partial round scoring, by deriving the
  rank label from the total match count per round rather than the dynamic
  still-alive count
- Discord notifications now fire when a drafted participant is eliminated
  with 0 points (e.g. R64 losers whose team score doesn't change)
- Discord notifications no longer include all prior matches — scoped to
  the current batch via matchIds
- Discord section headers have spacing; rank medals replaced with numbers
- Rounds auto-complete when all matches are marked done; manual Complete
  Round dropdown removed
- Fixed double Discord notification when the last match in a round
  triggers auto-complete: processPlayoffEvent now accepts skipRecalculate
  so the caller controls when the notification fires
- Fixed falsy check on finalPosition=0 in calculateTeamScore
- Undrafted 0-point participants filtered from Eliminated Teams display

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 12:34:10 -07:00

177 lines
5.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { sendDiscordWebhook, sendStandingsUpdateNotification } from "../discord";
const WEBHOOK_URL = "https://discord.com/api/webhooks/123/abc";
function mockFetch(status: number, body = "") {
return vi.fn().mockResolvedValue({
ok: status >= 200 && status < 300,
status,
text: async () => body,
});
}
function getDescription(): string {
const body = JSON.parse(
(fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body
);
return body.embeds[0].description as string;
}
describe("sendDiscordWebhook", () => {
beforeEach(() => {
vi.stubGlobal("fetch", mockFetch(204));
});
it("POSTs JSON to the webhook URL", async () => {
await sendDiscordWebhook(WEBHOOK_URL, { content: "hello" });
expect(fetch).toHaveBeenCalledWith(WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: "hello" }),
});
});
it("throws on non-2xx response", async () => {
vi.stubGlobal("fetch", mockFetch(400, "Bad Request"));
await expect(sendDiscordWebhook(WEBHOOK_URL, {})).rejects.toThrow(
"Discord webhook failed: 400"
);
});
});
describe("sendStandingsUpdateNotification", () => {
beforeEach(() => {
vi.stubGlobal("fetch", mockFetch(204));
});
it("sends an embed with the season name as title", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 }],
previousStandings: new Map(),
});
const body = JSON.parse((fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body);
expect(body.embeds[0].title).toBe("📊 Standings Update — My League 2025");
});
it("shows point deltas as integers in description", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "Test League",
standings: [
{ teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 },
{ teamId: "b", teamName: "Beta", totalPoints: 100, rank: 2 },
],
previousStandings: new Map([
["a", 125],
["b", 100],
]),
});
const desc = getDescription();
expect(desc).toContain("Alpha");
expect(desc).toContain("+25 pts");
expect(desc).not.toContain("+25.0");
// Beta didn't change — no delta shown
expect(desc).toMatch(/Beta.*100 pts(?!\s*\()/);
});
it("includes sport name and event name in header when both provided", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
previousStandings: new Map(),
sportName: "UEFA Champions League",
eventName: "Knockout Stage",
});
const desc = getDescription();
expect(desc).toContain("UEFA Champions League — Knockout Stage");
});
it("shows only event name when no sport name provided", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
previousStandings: new Map(),
eventName: "Quarter Finals",
});
const desc = getDescription();
expect(desc).toContain("Quarter Finals");
expect(desc).not.toContain("—\n");
});
it("omits header section when neither sport name nor event name provided", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
previousStandings: new Map(),
});
const desc = getDescription();
// Description should start directly with the standings section
expect(desc).toContain("**Current Standings**");
expect(desc).not.toContain("**Scored Matches**");
});
it("lists scored matches above standings", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
previousStandings: new Map(),
scoredMatches: [
{ winnerName: "Real Madrid", loserName: "Bayern Munich" },
{ winnerName: "Arsenal", loserName: "PSG" },
],
});
const desc = getDescription();
expect(desc).toContain("**Scored Matches**");
expect(desc).toContain("• **Real Madrid** def. Bayern Munich");
expect(desc).toContain("• **Arsenal** def. PSG");
// Scored matches section should appear before standings
expect(desc.indexOf("Scored Matches")).toBeLessThan(desc.indexOf("Current Standings"));
});
it("uses plain numbers for all ranks in description", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [
{ teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 },
{ teamId: "b", teamName: "Beta", totalPoints: 125, rank: 2 },
{ teamId: "c", teamName: "Gamma", totalPoints: 100, rank: 3 },
{ teamId: "d", teamName: "Delta", totalPoints: 75, rank: 4 },
],
previousStandings: new Map(),
});
const desc = getDescription();
expect(desc).toContain("1. Alpha");
expect(desc).toContain("2. Beta");
expect(desc).toContain("3. Gamma");
expect(desc).toContain("4. Delta");
});
it("does not show fields on the embed", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
previousStandings: new Map(),
});
const body = JSON.parse((fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body);
expect(body.embeds[0].fields).toBeUndefined();
});
});