brackt/app/services/match-sync/__tests__/wikipedia-tennis.test.ts
Chris Parsons 81d063faa1
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m13s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m23s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
Auto-populate & auto-score tennis Grand Slam brackets from Wikipedia
Add a per-event "Sync Draw" that pulls a tennis major's full 128-player
draw from its Wikipedia article, auto-creates/links participants (and
propagates them to every linked sibling season), builds the bracket, and
runs the qualifying-points scorer. Re-running advances the bracket and
scoring as matches complete.

Core
- match-sync: WikipediaTennisAdapter + wikitext bracket parser, DrawSync
  DTOs, syncTennisDraw orchestrator, pure draw->rows mapping
- playoff-match: populateBracketFromDraw (idempotent upsert on externalMatchId)
- scoring_events.externalSourceKey column (Wikipedia article; migration 0123)
- admin bracket "Sync Draw" card (accepts URL or title) + cron pass

Scoring fix
- deriveBracketQualifyingStates only floors players who have reached the
  scoring stage; for deep brackets (tennis_128) early-round losers earn 0 QP
  instead of a phantom 9th-place floor. CS2/simple_8 behavior preserved
  (gated on rounds[0].isScoring). Re-sync reconciles stale QP rows in a
  transaction.

Matching & parsing
- accent-folding in normalizeTeamName; strip Wikipedia "(tennis)"
  disambiguators; treat Bye/TBD/Qualifier as TBD; extract wikilink before
  template-stripping so {{nowrap}}-wrapped players parse

Admin UX
- dry-run preview (matched / will-create / possible duplicates / unfilled
  slots) with inline "rename existing" / "create as new" resolution via
  fetcher (no full reload)

Tests: parser vs real 2025 Wimbledon fixture, draw mapping, tennis_128
scoring, accent/disambiguator/nowrap parsing, URL parsing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 21:48:00 -07:00

176 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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");
});
});