import { describe, it, expect } from "vitest"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { parseTennisDrawWikitext, parsePlayerCell, parseTemplateParams, extractTemplates, slugifyArticle, articleTitleFromInput, } from "../wikipedia-tennis"; import type { FetchedRound } from "../types"; const FIXTURE = readFileSync( join(__dirname, "fixtures", "wimbledon-2025-mens-singles.wikitext"), "utf-8", ); const ARTICLE = "2025 Wimbledon Championships – Men's singles"; function roundOrThrow(rounds: FetchedRound[], name: string): FetchedRound { const round = rounds.find((r) => r.roundName === name); if (!round) throw new Error(`round ${name} missing`); return round; } describe("parsePlayerCell", () => { it("extracts name + wikilink target and detects the bolded winner", () => { const cell = parsePlayerCell("'''{{flagicon|ITA}} [[Jannik Sinner]]'''"); expect(cell).toEqual({ player: { name: "Jannik Sinner", externalId: "Jannik Sinner", seed: null }, isWinner: true, }); }); it("treats a non-bolded slot as a loser", () => { const cell = parsePlayerCell("{{flagicon|USA}} [[Ben Shelton]]"); expect(cell?.isWinner).toBe(false); expect(cell?.player.name).toBe("Ben Shelton"); }); it("handles piped wikilinks (qualifier disambiguation)", () => { const cell = parsePlayerCell("{{flagicon|FRA}} [[Arthur Fils (tennis)|Arthur Fils]]"); expect(cell?.player.name).toBe("Arthur Fils"); expect(cell?.player.externalId).toBe("Arthur Fils (tennis)"); }); it("parses a player whose cell is wrapped in {{nowrap}} (long names)", () => { const loser = parsePlayerCell( "{{nowrap|{{flagicon|ESP}} [[Alejandro Davidovich Fokina|A Davidovich Fokina]]}}", ); expect(loser?.player.externalId).toBe("Alejandro Davidovich Fokina"); expect(loser?.player.name).toBe("A Davidovich Fokina"); expect(loser?.isWinner).toBe(false); const winner = parsePlayerCell( "'''{{nowrap|{{flagicon|ARG}} [[Juan Manuel Cerúndolo|JM Cerúndolo]]}}'''", ); expect(winner?.player.externalId).toBe("Juan Manuel Cerúndolo"); expect(winner?.isWinner).toBe(true); }); it("returns null for empty, Bye, and not-yet-determined placeholder slots", () => { expect(parsePlayerCell("")).toBeNull(); expect(parsePlayerCell("Bye")).toBeNull(); expect(parsePlayerCell("Qualifier")).toBeNull(); expect(parsePlayerCell("{{flagicon|GBR}} Qualifier")).toBeNull(); expect(parsePlayerCell("TBD")).toBeNull(); }); }); describe("parseTemplateParams", () => { it("splits on top-level pipes only, preserving piped links and templates", () => { const params = parseTemplateParams( "RD1-team1={{flagicon|ITA}} [[A B|A]]|RD1-seed1=1|RD1-score1-1=6", ); expect(params.get("RD1-team1")).toBe("{{flagicon|ITA}} [[A B|A]]"); expect(params.get("RD1-seed1")).toBe("1"); expect(params.get("RD1-score1-1")).toBe("6"); }); }); describe("extractTemplates", () => { it("finds the 8 section brackets and 1 finals bracket", () => { expect(extractTemplates(FIXTURE, ["16TeamBracket"]).length).toBe(8); expect(extractTemplates(FIXTURE, ["8TeamBracket"]).length).toBe(1); }); }); describe("parseTennisDrawWikitext (2025 Wimbledon men's singles)", () => { const draw = parseTennisDrawWikitext(FIXTURE, ARTICLE); it("maps to the tennis_128 template", () => { expect(draw.templateId).toBe("tennis_128"); }); it("produces the full 127-match draw with correct per-round counts", () => { const byRound = Object.fromEntries(draw.rounds.map((r) => [r.roundName, r.matches.length])); expect(byRound).toEqual({ "Round of 128": 64, "Round of 64": 32, "Round of 32": 16, "Round of 16": 8, Quarterfinals: 4, Semifinals: 2, Final: 1, }); const total = draw.rounds.reduce((sum, r) => sum + r.matches.length, 0); expect(total).toBe(127); }); it("populates both players in section rounds (not just the finals bracket)", () => { const r128 = roundOrThrow(draw.rounds, "Round of 128"); // A full 128 draw has no byes — every R128 slot is filled. const populated = r128.matches.filter((m) => m.player1 && m.player2); expect(populated.length).toBe(64); // Section brackets abbreviate display names ("J Sinner") but the wikilink // target stays the full name — so externalId is the reliable key. const sinnerMatch = r128.matches.find( (m) => m.player1?.externalId === "Jannik Sinner" || m.player2?.externalId === "Jannik Sinner", ); expect(sinnerMatch).toBeDefined(); const sinner = sinnerMatch?.player1?.externalId === "Jannik Sinner" ? sinnerMatch?.player1 : sinnerMatch?.player2; expect(sinner?.name).toBe("J Sinner"); expect(sinner?.seed).toBe(1); }); it("identifies Sinner as the champion (won the Final)", () => { const final = roundOrThrow(draw.rounds, "Final").matches[0]; const winner = final.winner === 1 ? final.player1 : final.player2; expect(winner?.name).toBe("Jannik Sinner"); }); it("gives every match a unique externalMatchId", () => { const ids = draw.rounds.flatMap((r) => r.matches.map((m) => m.externalMatchId)); expect(new Set(ids).size).toBe(ids.length); }); it("records seeds for the 32 seeded players in round 1", () => { const r128 = roundOrThrow(draw.rounds, "Round of 128"); const seeded = r128.matches .flatMap((m) => [m.player1, m.player2]) .filter((p) => typeof p?.seed === "number"); expect(seeded.length).toBe(32); }); }); describe("articleTitleFromInput", () => { it("extracts the title from a pasted Wikipedia URL", () => { expect( articleTitleFromInput( "https://en.wikipedia.org/wiki/2025_Wimbledon_Championships_%E2%80%93_Men%27s_singles", ), ).toBe("2025 Wimbledon Championships – Men's singles"); }); it("strips query strings and fragments", () => { expect(articleTitleFromInput("https://en.wikipedia.org/wiki/French_Open?action=raw#Draw")).toBe( "French Open", ); }); it("passes a plain title through unchanged", () => { expect(articleTitleFromInput(" 2025 US Open – Men's singles ")).toBe( "2025 US Open – Men's singles", ); }); }); describe("slugifyArticle", () => { it("produces an id-safe slug and strips diacritics", () => { expect(slugifyArticle(ARTICLE)).toBe("2025-wimbledon-championships-men-s-singles"); }); });