177 lines
6.4 KiB
TypeScript
177 lines
6.4 KiB
TypeScript
|
|
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");
|
|||
|
|
});
|
|||
|
|
});
|