When a match is displayed because the loser was eliminated (0-pt), the winner's manager was previously omitted if the winner only advanced without earning points. Now the winner's owner is always credited on any match that passes the notability gate, while the gate itself (winnerScoreChanged || loserNotifiable) still prevents purely-advancing matches from appearing. Adds isMatchNotable() pure helper alongside isLoserNotifiable() to make the display rule testable in isolation. Co-Authored-By: Claude <noreply@anthropic.com>
769 lines
26 KiB
TypeScript
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 owner even when winner only advanced (loser was eliminated)", async () => {
|
|
// Mirrors the Brazil/Japan scenario: Brazil advanced without earning points,
|
|
// Japan was eliminated. The calculator now sets winnerUsername unconditionally
|
|
// on any match that passes the notability gate.
|
|
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: "someManager",
|
|
loserUsername: "ikyn",
|
|
},
|
|
],
|
|
});
|
|
|
|
const desc = getDescription();
|
|
expect(desc).toContain("• **Brazil (someManager)** 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");
|
|
});
|
|
});
|