brackt/app/services/__tests__/discord.test.ts
Claude 613106cb9e
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m56s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m18s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
Fix notification filter: only include matches where an owner earned points or lost
The previous filter let matches through if showLoser was true (loser got a
finalized result) regardless of whether the loser or winner was actually
owned by a manager. This caused spurious notifications when a managed team
advanced through a non-scoring round (e.g. R32 → R16 in World Cup) while
beating an unowned opponent who happened to get a 0-pt finalized result.

New rule: a match is worth announcing only when the winner is owned AND
earned points, OR the loser is owned. When a match fires because the loser
is owned, the winner's manager tag is still shown for context but they are
not Discord-pinged (winnerDiscordUserId remains gated on winnerScoreChanged).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXKpR3MZJn5Zof1AKeETFM
2026-06-30 23:40:41 +00:00

769 lines
26 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { sendDiscordWebhook, sendStandingsUpdateNotification, sendDraftOrderNotification, sendPickAnnouncementNotification } 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;
}
function getPayload(): ReturnType<typeof JSON.parse> {
return JSON.parse((fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body);
}
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([["a", 125]]),
});
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 standings changes", 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 — not shown in standings changes
expect(desc).not.toContain("Beta");
});
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: 125, rank: 1 }],
previousStandings: new Map([["a", 100]]),
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: 125, rank: 1 }],
previousStandings: new Map([["a", 100]]),
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: 125, rank: 1 }],
previousStandings: new Map([["a", 100]]),
});
const desc = getDescription();
expect(desc).toContain("**Standings Changes**");
expect(desc).not.toContain("**Scored Matches**");
});
it("lists scored matches above standings changes", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
previousStandings: new Map([["a", 100]]),
scoredMatches: [
{ winnerName: "Real Madrid", loserName: "Bayern Munich", winnerUsername: "manager1" },
{ winnerName: "Arsenal", loserName: "PSG", loserUsername: "manager2" },
],
});
const desc = getDescription();
expect(desc).toContain("**Scored Matches**");
expect(desc).toContain("• **Real Madrid (manager1)** def. Bayern Munich");
expect(desc).toContain("• **Arsenal** def. PSG (manager2)");
// Scored matches section should appear before standings
expect(desc.indexOf("Scored Matches")).toBeLessThan(desc.indexOf("Standings Changes"));
});
it("shows username in parentheses in standings changes when provided", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [
{ teamId: "a", teamName: "Alpha FC", username: "christhrowsrocks", totalPoints: 150, rank: 1 },
{ teamId: "b", teamName: "Beta United", totalPoints: 100, rank: 2 },
],
previousStandings: new Map([
["a", 125],
["b", 100],
]),
});
const desc = getDescription();
expect(desc).toContain("1\\. Alpha FC (christhrowsrocks) — 150 pts");
// Beta didn't change points or rank, so it's not shown
expect(desc).not.toContain("Beta United");
});
it("shows usernames in parentheses in match results when both are drafted", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
previousStandings: new Map([["a", 100]]),
scoredMatches: [
{
winnerName: "Sporting",
loserName: "Bodø/Glimt",
winnerUsername: "christhrowsrocks",
loserUsername: "apatel",
},
],
});
const desc = getDescription();
expect(desc).toContain("• **Sporting (christhrowsrocks)** def. Bodø/Glimt (apatel)");
});
it("shows winner's manager for context when the match fires due to an owned loser", async () => {
// Brazil beats Japan (R32, non-scoring). Japan's manager is the reason for the
// notification; Brazil's manager is shown for context even though they didn't score.
// scoring-calculator.ts controls whether to include the match; discord.ts just renders.
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "Rumble League 2026",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
previousStandings: new Map([["a", 100]]),
scoredMatches: [
{
winnerName: "Brazil",
loserName: "Japan",
winnerUsername: "aliceManager",
loserUsername: "ikyn",
},
],
});
const desc = getDescription();
expect(desc).toContain("• **Brazil (aliceManager)** def. Japan (ikyn)");
});
it("omits scored matches where neither side has a username", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
previousStandings: new Map([["a", 100]]),
scoredMatches: [{ winnerName: "Real Madrid", loserName: "Bayern Munich" }],
});
const desc = getDescription();
expect(desc).not.toContain("**Scored Matches**");
expect(desc).not.toContain("Real Madrid");
expect(desc).not.toContain("Bayern Munich");
});
it("omits username parenthesis only for the undrafted side in match results", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
previousStandings: new Map([["a", 100]]),
scoredMatches: [
{
winnerName: "Sporting",
loserName: "Bodø/Glimt",
winnerUsername: "christhrowsrocks",
},
],
});
const desc = getDescription();
expect(desc).toContain("• **Sporting (christhrowsrocks)** def. Bodø/Glimt");
expect(desc).not.toContain("Bodø/Glimt (");
});
it("shows teams that changed rank even without a point change", 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 },
],
previousStandings: new Map([
["a", 125],
["b", 125],
["c", 100],
]),
previousRanks: new Map([
["a", 2],
["b", 1],
["c", 3],
]),
});
const desc = getDescription();
// Alpha gained points and moved up
expect(desc).toContain("Alpha");
expect(desc).toContain("+25 pts");
expect(desc).toContain("↑1");
// Beta didn't gain points but was displaced — rank changed
expect(desc).toContain("Beta");
expect(desc).toContain("↓1");
// Gamma unchanged — not shown
expect(desc).not.toContain("Gamma");
});
it("omits rank delta when no previousRanks provided", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [
{ teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 },
],
previousStandings: new Map([["a", 125]]),
});
const desc = getDescription();
expect(desc).toContain("Alpha");
expect(desc).not.toContain("↑");
expect(desc).not.toContain("↓");
});
it("escapes Discord markdown characters in team names and usernames", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [
{ teamId: "a", teamName: "Team__Underline", username: "user__name", totalPoints: 150, rank: 1 },
],
previousStandings: new Map([["a", 125]]),
scoredMatches: [
{
winnerName: "Winner*Bold*",
loserName: "Loser~Strike~",
winnerUsername: "user_one",
loserUsername: "user_two",
},
],
});
const desc = getDescription();
expect(desc).toContain("Team\\_\\_Underline");
expect(desc).toContain("user\\_\\_name");
expect(desc).toContain("Winner\\*Bold\\*");
expect(desc).toContain("Loser\\~Strike\\~");
expect(desc).toContain("user\\_one");
expect(desc).toContain("user\\_two");
});
it("uses Discord mention in standings for opted-in user", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [
{
teamId: "a",
teamName: "Alpha FC",
username: "christhrowsrocks",
discordUserId: "111222333",
totalPoints: 150,
rank: 1,
},
],
previousStandings: new Map([["a", 125]]),
});
const desc = getDescription();
expect(desc).toContain("Alpha FC (<@111222333>)");
expect(desc).not.toContain("christhrowsrocks");
});
it("uses Discord mention in scored match for opted-in winner", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
previousStandings: new Map([["a", 100]]),
scoredMatches: [
{
winnerName: "Real Madrid",
loserName: "Bayern Munich",
winnerUsername: "manager1",
winnerDiscordUserId: "111222333",
loserUsername: "manager2",
},
],
});
const desc = getDescription();
expect(desc).toContain("Real Madrid (<@111222333>)");
expect(desc).not.toContain("manager1");
expect(desc).toContain("Bayern Munich (manager2)");
});
it("sets content and allowed_mentions when opted-in users are present", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [
{
teamId: "a",
teamName: "Alpha FC",
username: "christhrowsrocks",
discordUserId: "111222333",
totalPoints: 150,
rank: 1,
},
],
previousStandings: new Map([["a", 125]]),
});
const payload = getPayload();
expect(payload.content).toBe("<@111222333>");
expect(payload.allowed_mentions).toEqual({ parse: [], users: ["111222333"] });
});
it("omits content and allowed_mentions when no users opted in", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [
{ teamId: "a", teamName: "Alpha FC", username: "christhrowsrocks", totalPoints: 150, rank: 1 },
],
previousStandings: new Map([["a", 125]]),
});
const payload = getPayload();
expect(payload.content).toBeUndefined();
expect(payload.allowed_mentions).toBeUndefined();
});
it("collects Discord IDs from both standings and scored matches", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [
{ teamId: "a", teamName: "Alpha", username: "user1", discordUserId: "111", totalPoints: 150, rank: 1 },
],
previousStandings: new Map([["a", 125]]),
scoredMatches: [
{
winnerName: "Real Madrid",
loserName: "Bayern Munich",
winnerUsername: "user2",
winnerDiscordUserId: "222",
loserUsername: "user1",
loserDiscordUserId: "111",
},
],
});
const payload = getPayload();
const ids: string[] = payload.allowed_mentions.users;
expect(ids).toHaveLength(2);
expect(ids).toContain("111");
expect(ids).toContain("222");
});
it("does not show fields on the embed", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
previousStandings: new Map([["a", 100]]),
});
const body = JSON.parse((fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body);
expect(body.embeds[0].fields).toBeUndefined();
});
it("omits standings changes section when no teams changed", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [
{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 },
{ teamId: "b", teamName: "Beta", totalPoints: 75, rank: 2 },
],
previousStandings: new Map([
["a", 100],
["b", 75],
]),
previousRanks: new Map([
["a", 1],
["b", 2],
]),
});
const desc = getDescription();
expect(desc).not.toContain("**Standings Changes**");
expect(desc).not.toContain("Alpha");
expect(desc).not.toContain("Beta");
});
it("lists eliminated teams with their managers", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
previousStandings: new Map([["a", 100]]),
eliminatedTeams: [
{ participantName: "Phoenix Suns", username: "manager1" },
{ participantName: "Toronto Raptors" },
],
});
const desc = getDescription();
expect(desc).toContain("**Eliminated**");
expect(desc).toContain("• **Phoenix Suns (manager1)**");
expect(desc).toContain("• **Toronto Raptors**");
});
it("uses Discord mention for opted-in eliminated managers and pings them", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
previousStandings: new Map([["a", 100]]),
eliminatedTeams: [
{ participantName: "Phoenix Suns", username: "manager1", discordUserId: "555" },
],
});
const desc = getDescription();
expect(desc).toContain("• **Phoenix Suns (<@555>)**");
expect(desc).not.toContain("manager1");
const payload = getPayload();
expect(payload.content).toBe("<@555>");
expect(payload.allowed_mentions).toEqual({ parse: [], users: ["555"] });
});
it("escapes Discord markdown in eliminated team names", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
previousStandings: new Map([["a", 100]]),
eliminatedTeams: [{ participantName: "Team__Bold", username: "user_name" }],
});
const desc = getDescription();
expect(desc).toContain("Team\\_\\_Bold");
expect(desc).toContain("user\\_name");
});
it("omits the eliminated section when none provided", async () => {
await sendStandingsUpdateNotification({
webhookUrl: WEBHOOK_URL,
seasonName: "My League 2025",
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 }],
previousStandings: new Map([["a", 125]]),
});
const desc = getDescription();
expect(desc).not.toContain("**Eliminated**");
});
});
describe("sendDraftOrderNotification", () => {
beforeEach(() => {
vi.stubGlobal("fetch", mockFetch(204));
});
it("uses '📋 Draft Order Manually Set' title for manual method", async () => {
await sendDraftOrderNotification({
webhookUrl: WEBHOOK_URL,
leagueName: "My League",
leagueUrl: "https://brackt.com/leagues/abc",
method: "manual",
teams: [{ name: "Team Alpha", position: 1 }],
});
const payload = getPayload();
expect(payload.embeds[0].title).toBe("Draft Order Manually Set — My League");
});
it("uses '🎲 Draft Order Randomized' title for randomized method", async () => {
await sendDraftOrderNotification({
webhookUrl: WEBHOOK_URL,
leagueName: "My League",
leagueUrl: "https://brackt.com/leagues/abc",
method: "randomized",
teams: [{ name: "Team Alpha", position: 1 }],
});
const payload = getPayload();
expect(payload.embeds[0].title).toBe("Draft Order Randomized — My League");
});
it("links the embed title to the league URL", async () => {
await sendDraftOrderNotification({
webhookUrl: WEBHOOK_URL,
leagueName: "My League",
leagueUrl: "https://brackt.com/leagues/abc",
method: "manual",
teams: [{ name: "Team Alpha", position: 1 }],
});
const payload = getPayload();
expect(payload.embeds[0].url).toBe("https://brackt.com/leagues/abc");
});
it("renders a numbered list of teams sorted by position", async () => {
await sendDraftOrderNotification({
webhookUrl: WEBHOOK_URL,
leagueName: "My League",
leagueUrl: "https://brackt.com/leagues/abc",
method: "manual",
teams: [
{ name: "Team Gamma", position: 3 },
{ name: "Team Alpha", position: 1 },
{ name: "Team Beta", position: 2 },
],
});
const payload = getPayload();
expect(payload.embeds[0].description).toBe(
"1. Team Alpha\n2. Team Beta\n3. Team Gamma"
);
});
it("shows username in parentheses next to team name when provided", async () => {
await sendDraftOrderNotification({
webhookUrl: WEBHOOK_URL,
leagueName: "My League",
leagueUrl: "https://brackt.com/leagues/abc",
method: "manual",
teams: [
{ name: "Team Alpha", position: 1, username: "chris" },
{ name: "Team Beta", position: 2 },
],
});
const payload = getPayload();
expect(payload.embeds[0].description).toContain("1. Team Alpha (chris)");
expect(payload.embeds[0].description).toContain("2. Team Beta");
expect(payload.embeds[0].description).not.toContain("2. Team Beta (");
});
it("escapes Discord markdown in team names and usernames", async () => {
await sendDraftOrderNotification({
webhookUrl: WEBHOOK_URL,
leagueName: "My League",
leagueUrl: "https://brackt.com/leagues/abc",
method: "manual",
teams: [
{ name: "Team__Underline", position: 1, username: "user__name" },
{ name: "Team*Bold*", position: 2 },
],
});
const payload = getPayload();
expect(payload.embeds[0].description).toContain("Team\\_\\_Underline");
expect(payload.embeds[0].description).toContain("user\\_\\_name");
expect(payload.embeds[0].description).toContain("Team\\*Bold\\*");
});
it("uses color 0x5865f2 and has no footer", async () => {
await sendDraftOrderNotification({
webhookUrl: WEBHOOK_URL,
leagueName: "My League",
leagueUrl: "https://brackt.com/leagues/abc",
method: "manual",
teams: [{ name: "Team Alpha", position: 1 }],
});
const payload = getPayload();
expect(payload.embeds[0].color).toBe(0x5865f2);
expect(payload.embeds[0].footer).toBeUndefined();
});
it("does not set content or allowed_mentions", async () => {
await sendDraftOrderNotification({
webhookUrl: WEBHOOK_URL,
leagueName: "My League",
leagueUrl: "https://brackt.com/leagues/abc",
method: "manual",
teams: [{ name: "Team Alpha", position: 1 }],
});
const payload = getPayload();
expect(payload.content).toBeUndefined();
expect(payload.allowed_mentions).toBeUndefined();
});
it("renders bare team names with no trailing parentheses when no team has an owner", async () => {
await sendDraftOrderNotification({
webhookUrl: WEBHOOK_URL,
leagueName: "My League",
leagueUrl: "https://brackt.com/leagues/abc",
method: "manual",
teams: [
{ name: "Team Alpha", position: 1 },
{ name: "Team Beta", position: 2 },
],
});
const payload = getPayload();
expect(payload.embeds[0].description).toBe("1. Team Alpha\n2. Team Beta");
expect(payload.embeds[0].description).not.toContain("(");
});
});
describe("sendPickAnnouncementNotification", () => {
const BASE = {
webhookUrl: WEBHOOK_URL,
draftUrl: "https://brackt.com/leagues/abc/draft/season-1",
pickNumber: 5,
round: 1,
pickInRound: 5,
pickedTeamName: "Alpha FC",
participantName: "Erling Haaland",
sportName: "Soccer",
isDraftComplete: false,
};
beforeEach(() => {
vi.stubGlobal("fetch", mockFetch(204));
});
it("sets embed title with pick and round info", async () => {
await sendPickAnnouncementNotification(BASE);
const payload = getPayload();
expect(payload.embeds[0].title).toBe("Pick #5 — Round 1, Pick 5");
});
it("sets embed url to the draft room link", async () => {
await sendPickAnnouncementNotification(BASE);
const payload = getPayload();
expect(payload.embeds[0].url).toBe(BASE.draftUrl);
});
it("description contains team, participant, and sport", async () => {
await sendPickAnnouncementNotification(BASE);
const desc = getDescription();
expect(desc).toContain("**Alpha FC**");
expect(desc).toContain("**Erling Haaland**");
expect(desc).toContain("(Soccer)");
});
it("uses Discord blurple color and no footer", async () => {
await sendPickAnnouncementNotification(BASE);
const payload = getPayload();
expect(payload.embeds[0].color).toBe(0x5865f2);
expect(payload.embeds[0].footer).toBeUndefined();
});
it("shows next team when provided", async () => {
await sendPickAnnouncementNotification({ ...BASE, nextTeamName: "Beta United" });
const desc = getDescription();
expect(desc).toContain("On the clock: **Beta United**");
});
it("pings next owner with Discord mention when Discord ID is provided", async () => {
await sendPickAnnouncementNotification({
...BASE,
nextTeamName: "Beta United",
nextOwnerDiscordId: "999888777",
});
const desc = getDescription();
expect(desc).toContain("<@999888777>");
const payload = getPayload();
expect(payload.content).toBe("<@999888777>");
expect(payload.allowed_mentions).toEqual({ parse: [], users: ["999888777"] });
});
it("shows team name without mention when no Discord ID", async () => {
await sendPickAnnouncementNotification({ ...BASE, nextTeamName: "Beta United" });
const payload = getPayload();
expect(payload.content).toBeUndefined();
expect(payload.allowed_mentions).toBeUndefined();
});
it("shows draft complete message when isDraftComplete is true", async () => {
await sendPickAnnouncementNotification({ ...BASE, isDraftComplete: true });
const desc = getDescription();
expect(desc).toContain("The draft is complete!");
expect(desc).not.toContain("On the clock");
const payload = getPayload();
expect(payload.content).toBeUndefined();
expect(payload.allowed_mentions).toBeUndefined();
});
it("escapes markdown in team and participant names", async () => {
await sendPickAnnouncementNotification({
...BASE,
pickedTeamName: "Alpha_FC",
participantName: "Player*Name",
sportName: "E-Sports",
});
const desc = getDescription();
expect(desc).toContain("Alpha\\_FC");
expect(desc).toContain("Player\\*Name");
});
});