Review caught that the generic feeder rule was being applied to templates that route by their own logic. It was harmless as dead code; driving the renderer with it made several brackets worse than before. The rule pairs rounds by array order and assumes match n is fed by 2n-1 and 2n. That describes advanceWinnerTemplate, not every bracket: - afl_10's Wildcard Round feeds the Elimination Finals, skipping the round listed next to it, so array order fabricated the entire chain and drew ten wrong connectors contradicting advanceAFLWinner. - fifa_48's Third Place Game sits between the Semifinals and the Finals, so the Finals came out fed by the third place game. Once BracketTreeView filtered the consolation round out, the group had three roots and the whole World Cup bracket rendered with no connectors at all. - ncaa_68 labelled Round of 64 #1/#2 with First Four feeds that advanceFirstFourWinner doesn't use. - nba_20's play-in halves in size but pairs the 7v8 loser with the 9v10 winner. Follow each round's declared feedsInto, and derive edges only where the round halves exactly — the condition under which the generic ceil(n/2) mapping is true. Bespoke transitions that happen to halve are named explicitly. Slots left without a feeder read TBD, which is honest. Dropping those edges sends the group to the fallback, so the fallback now has to keep drawing what those brackets already drew: halving U-shapes by round size, and winner tracing through irregular shapes. Previously it drew nothing, which also silently removed every connector from brackets with no bracketTemplateId. Also from review: - clear-bracket deleted seasonParticipantResults for the entire sports season with no rebuild. That table is keyed by season, not event, so it wiped placements for every other event in the season — permanently zeroing standings on a finalized qualifying season. Delete only the matches and point the admin at Reprocess Bracket, which rebuilds placements correctly. - The clear-bracket form sent confirm=true from a hidden field, making the server's completed-match guard unreachable. It's a checkbox now, so the guard is real, including without JS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
533 lines
20 KiB
TypeScript
533 lines
20 KiB
TypeScript
/**
|
|
* Bracket layout tests.
|
|
*
|
|
* The load-bearing assertions check the LLWS geometry against the official 2026 LLBWS
|
|
* bracket, in the PDF's own game numbers. A bracket "lines up" when each card sits level
|
|
* with the game that feeds it, so these tests assert column membership, top-to-bottom
|
|
* order, and vertical alignment — not just that a layout was produced.
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import {
|
|
LLWS_20,
|
|
SIMPLE_16,
|
|
NFL_14,
|
|
BRACKET_TEMPLATES,
|
|
getBracketTemplate,
|
|
type BracketTemplate,
|
|
type ConferenceGroup,
|
|
} from "~/lib/bracket-templates";
|
|
import {
|
|
buildFeederMap,
|
|
computeGroupLayout,
|
|
describeSlotSource,
|
|
matchKey,
|
|
type SlotSource,
|
|
} from "~/lib/bracket-layout";
|
|
import { GAME_TO_MATCH, MATCH_TO_GAME } from "~/test/fixtures/llws-bracket";
|
|
|
|
interface TestMatch {
|
|
round: string;
|
|
matchNumber: number;
|
|
/** Only the fallback reads these, to trace edges through an unrecognised shape. */
|
|
winnerId?: string | null;
|
|
participant1Id?: string | null;
|
|
participant2Id?: string | null;
|
|
}
|
|
|
|
/** Every match a template defines, as the renderer would receive them. */
|
|
function allMatches(template: BracketTemplate): Map<string, TestMatch[]> {
|
|
const byRound = new Map<string, TestMatch[]>();
|
|
for (const round of template.rounds) {
|
|
byRound.set(
|
|
round.name,
|
|
Array.from({ length: round.matchCount }, (_, i) => ({
|
|
round: round.name,
|
|
matchNumber: i + 1,
|
|
}))
|
|
);
|
|
}
|
|
return byRound;
|
|
}
|
|
|
|
/** The matches of one phase group, filtered the way TabbedBracketLayout filters them. */
|
|
function groupMatches(group: ConferenceGroup): Map<string, TestMatch[]> {
|
|
const byRound = new Map<string, TestMatch[]>();
|
|
for (const [round, nums] of Object.entries(group.roundMatchNumbers)) {
|
|
byRound.set(
|
|
round,
|
|
nums.map((matchNumber) => ({ round, matchNumber }))
|
|
);
|
|
}
|
|
return byRound;
|
|
}
|
|
|
|
function findGroup(name: string): ConferenceGroup {
|
|
for (const phase of LLWS_20.phases ?? []) {
|
|
for (const group of phase.groups ?? []) {
|
|
if (group.name === name) return group;
|
|
}
|
|
}
|
|
throw new Error(`No LLWS group named ${name}`);
|
|
}
|
|
|
|
/** Lay out one LLWS group and describe it in PDF game numbers. */
|
|
function layOutLLWSGroup(name: string) {
|
|
const group = findGroup(name);
|
|
const byRound = groupMatches(group);
|
|
const roundOrder = LLWS_20.rounds.map((r) => r.name);
|
|
const rounds = roundOrder.filter((r) => byRound.has(r));
|
|
|
|
const layout = computeGroupLayout(rounds, byRound, buildFeederMap(LLWS_20), roundOrder);
|
|
|
|
const game = (m: TestMatch) => {
|
|
const n = MATCH_TO_GAME.get(`${m.round}#${m.matchNumber}`);
|
|
if (n === undefined) throw new Error(`No PDF game for ${m.round} #${m.matchNumber}`);
|
|
return n;
|
|
};
|
|
|
|
return {
|
|
layout,
|
|
labels: layout.columns.map((c) => c.label),
|
|
/** Column contents, top to bottom, as PDF game numbers. */
|
|
columns: layout.columns.map((c) => c.matches.map((m) => game(m.match))),
|
|
/** Vertical centre of a game's card, in leaf-row units. */
|
|
centerOf(gameNumber: number): number {
|
|
const target = GAME_TO_MATCH[gameNumber];
|
|
for (const column of layout.columns) {
|
|
for (const { match, center } of column.matches) {
|
|
if (match.round === target.round && match.matchNumber === target.matchNumber) {
|
|
return center;
|
|
}
|
|
}
|
|
}
|
|
throw new Error(`G${gameNumber} is not in this group`);
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("computeGroupLayout — LLWS winners brackets", () => {
|
|
// The International side is the one in the reported screenshot. Under the old index
|
|
// math, G5 and G7 were stranded in the first column: they skip Winners Round 2 and go
|
|
// straight to the semifinals, so nothing in column two lined up with them.
|
|
it("puts the International winners bracket in the printed bracket's columns", () => {
|
|
const { columns } = layOutLLWSGroup("International Winner's Bracket");
|
|
expect(columns).toEqual([
|
|
[1, 3],
|
|
[5, 9, 11, 7],
|
|
[18, 20],
|
|
[29],
|
|
]);
|
|
});
|
|
|
|
it("mirrors that layout on the U.S. side", () => {
|
|
const { columns } = layOutLLWSGroup("U.S. Winner's Bracket");
|
|
expect(columns).toEqual([
|
|
[2, 4],
|
|
[6, 10, 12, 8],
|
|
[17, 19],
|
|
[30],
|
|
]);
|
|
});
|
|
|
|
it("names a mixed column for the latest round it holds", () => {
|
|
// Column two holds two Opening Round games (G5, G7) alongside Winners Round 2.
|
|
const { labels } = layOutLLWSGroup("International Winner's Bracket");
|
|
expect(labels).toEqual([
|
|
"Opening Round",
|
|
"Winners Round 2",
|
|
"Winners Semifinals",
|
|
"Winners Final",
|
|
]);
|
|
});
|
|
|
|
it("levels each card with the game that feeds it", () => {
|
|
const { centerOf } = layOutLLWSGroup("International Winner's Bracket");
|
|
|
|
// G1's winner fills a slot of G9, so the two sit at the same height.
|
|
expect(centerOf(1)).toBe(centerOf(9));
|
|
expect(centerOf(3)).toBe(centerOf(11));
|
|
|
|
// G18 = W5 v W9, so it sits midway between them.
|
|
expect(centerOf(18)).toBe((centerOf(5) + centerOf(9)) / 2);
|
|
expect(centerOf(20)).toBe((centerOf(11) + centerOf(7)) / 2);
|
|
expect(centerOf(29)).toBe((centerOf(18) + centerOf(20)) / 2);
|
|
});
|
|
|
|
it("draws an edge for every in-group feed, played or not", () => {
|
|
const { layout } = layOutLLWSGroup("International Winner's Bracket");
|
|
// G9←G1, G11←G3, G18←{G5,G9}, G20←{G11,G7}, G29←{G18,G20}: 8 in-group edges.
|
|
expect(layout.edges).toHaveLength(8);
|
|
// Every edge crosses exactly one gutter, which is what makes them drawable.
|
|
for (const edge of layout.edges) {
|
|
expect(edge.fromColumn).toBeGreaterThanOrEqual(0);
|
|
expect(edge.fromColumn).toBeLessThan(layout.columns.length - 1);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("computeGroupLayout — LLWS elimination brackets", () => {
|
|
it("orders Elimination Round 3 the way the printed bracket does", () => {
|
|
// G31 = W27 v W25, so the later game is printed on top — the reverse of match
|
|
// number order, which is how the old index-based sort got it wrong.
|
|
const { columns } = layOutLLWSGroup("International Elimination Bracket");
|
|
expect(columns).toEqual([
|
|
[13, 15],
|
|
[21, 23],
|
|
[27, 25],
|
|
[31],
|
|
[33],
|
|
]);
|
|
});
|
|
|
|
it("orders the U.S. elimination bracket the same way", () => {
|
|
const { columns } = layOutLLWSGroup("U.S. Elimination Bracket");
|
|
expect(columns).toEqual([
|
|
[14, 16],
|
|
[22, 24],
|
|
[28, 26],
|
|
[32],
|
|
[34],
|
|
]);
|
|
});
|
|
|
|
it("ignores feeds arriving from the winners bracket", () => {
|
|
// G21 = L9 v W13. L9 is in the winners bracket group, so only W13 is an edge here.
|
|
const { layout, centerOf } = layOutLLWSGroup("International Elimination Bracket");
|
|
expect(centerOf(21)).toBe(centerOf(13));
|
|
expect(layout.edges).toHaveLength(7);
|
|
});
|
|
});
|
|
|
|
describe("buildFeederMap", () => {
|
|
it("routes LLWS winners and losers to the slots the printed bracket shows", () => {
|
|
const feeders = buildFeederMap(LLWS_20);
|
|
|
|
// G18 = W5 v W9.
|
|
const g18 = GAME_TO_MATCH[18];
|
|
expect(feeders.get(matchKey(g18.round, g18.matchNumber))).toEqual([
|
|
{ kind: "match", ref: GAME_TO_MATCH[5], result: "winner" },
|
|
{ kind: "match", ref: GAME_TO_MATCH[9], result: "winner" },
|
|
]);
|
|
|
|
// G13 = L3 v L5 — a winners-bracket loss drops into the elimination bracket.
|
|
const g13 = GAME_TO_MATCH[13];
|
|
expect(feeders.get(matchKey(g13.round, g13.matchNumber))).toEqual([
|
|
{ kind: "match", ref: GAME_TO_MATCH[3], result: "loser" },
|
|
{ kind: "match", ref: GAME_TO_MATCH[5], result: "loser" },
|
|
]);
|
|
});
|
|
|
|
it("marks directly seeded slots as seeds", () => {
|
|
const feeders = buildFeederMap(LLWS_20);
|
|
// G9 = a bye team v W1: slot one is seeded, slot two is fed.
|
|
const g9 = GAME_TO_MATCH[9];
|
|
const [p1, p2] = feeders.get(matchKey(g9.round, g9.matchNumber)) ?? [];
|
|
expect(p1).toEqual({ kind: "seed" });
|
|
expect(p2).toEqual({ kind: "match", ref: GAME_TO_MATCH[1], result: "winner" });
|
|
});
|
|
|
|
it("applies the standard halving rule to other templates", () => {
|
|
const feeders = buildFeederMap(SIMPLE_16);
|
|
expect(feeders.get(matchKey("Quarterfinals", 1))).toEqual([
|
|
{ kind: "match", ref: { round: "Round of 16", matchNumber: 1 }, result: "winner" },
|
|
{ kind: "match", ref: { round: "Round of 16", matchNumber: 2 }, result: "winner" },
|
|
]);
|
|
expect(feeders.get(matchKey("Quarterfinals", 4))).toEqual([
|
|
{ kind: "match", ref: { round: "Round of 16", matchNumber: 7 }, result: "winner" },
|
|
{ kind: "match", ref: { round: "Round of 16", matchNumber: 8 }, result: "winner" },
|
|
]);
|
|
// The first round is seeded, not fed.
|
|
expect(feeders.get(matchKey("Round of 16", 1))).toEqual([
|
|
{ kind: "seed" },
|
|
{ kind: "seed" },
|
|
]);
|
|
});
|
|
|
|
it("returns an empty map without a template", () => {
|
|
expect(buildFeederMap(undefined).size).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("computeGroupLayout — standard brackets are unchanged", () => {
|
|
it("halves a 16-team bracket evenly, first round in seeded order", () => {
|
|
const byRound = allMatches(SIMPLE_16);
|
|
const roundOrder = SIMPLE_16.rounds.map((r) => r.name);
|
|
const layout = computeGroupLayout(
|
|
roundOrder,
|
|
byRound,
|
|
buildFeederMap(SIMPLE_16),
|
|
roundOrder
|
|
);
|
|
|
|
expect(layout.leafCount).toBe(8);
|
|
expect(layout.columns.map((c) => c.label)).toEqual(roundOrder);
|
|
expect(layout.columns.map((c) => c.matches.map((m) => m.match.matchNumber))).toEqual([
|
|
[1, 2, 3, 4, 5, 6, 7, 8],
|
|
[1, 2, 3, 4],
|
|
[1, 2],
|
|
[1],
|
|
]);
|
|
// Evenly spread, exactly as the previous index math placed them.
|
|
expect(layout.columns[0].matches.map((m) => m.center)).toEqual([
|
|
0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5,
|
|
]);
|
|
expect(layout.columns[3].matches[0].center).toBe(4);
|
|
});
|
|
|
|
it("handles byes, placing a seeded team level with the round it enters", () => {
|
|
// The NFL bracket's top seeds skip the wild card round.
|
|
const byRound = allMatches(NFL_14);
|
|
const roundOrder = NFL_14.rounds.map((r) => r.name);
|
|
const layout = computeGroupLayout(roundOrder, byRound, buildFeederMap(NFL_14), roundOrder);
|
|
expect(layout.columns.length).toBeGreaterThan(0);
|
|
for (const column of layout.columns) {
|
|
expect(column.matches.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it("falls back to even spreading when a group has no single root", () => {
|
|
// Two finals and no way to join them — the shape can't resolve to one tree.
|
|
const byRound = new Map<string, TestMatch[]>([
|
|
["Semifinals", [{ round: "Semifinals", matchNumber: 1 }]],
|
|
[
|
|
"Finals",
|
|
[
|
|
{ round: "Finals", matchNumber: 1 },
|
|
{ round: "Finals", matchNumber: 2 },
|
|
],
|
|
],
|
|
]);
|
|
const layout = computeGroupLayout(
|
|
["Semifinals", "Finals"],
|
|
byRound,
|
|
new Map(),
|
|
["Semifinals", "Finals"]
|
|
);
|
|
expect(layout.columns.map((c) => c.label)).toEqual(["Semifinals", "Finals"]);
|
|
expect(layout.edges).toEqual([]);
|
|
expect(layout.columns[0].matches[0].center).toBe(1);
|
|
expect(layout.columns[1].matches.map((m) => m.center)).toEqual([0.5, 1.5]);
|
|
});
|
|
|
|
it("returns nothing for an empty group", () => {
|
|
const layout = computeGroupLayout([], new Map(), new Map(), []);
|
|
expect(layout).toEqual({ columns: [], leafCount: 0, edges: [] });
|
|
});
|
|
});
|
|
|
|
describe("buildFeederMap — templates with routing of their own", () => {
|
|
// The halving rule describes advanceWinnerTemplate, not every bracket. Inventing it
|
|
// where it doesn't hold draws confident, wrong connectors and mislabels slots, which
|
|
// is worse than drawing nothing.
|
|
|
|
it("follows feedsInto rather than the order rounds are listed in", () => {
|
|
// AFL's Wildcard Round feeds the Elimination Finals, skipping the round printed
|
|
// next to it, so array order would fabricate the whole chain.
|
|
const afl = BRACKET_TEMPLATES.afl_10;
|
|
const feeders = buildFeederMap(afl);
|
|
const fed = [...feeders.entries()].filter(([, pair]) =>
|
|
pair.some((s) => s.kind === "match")
|
|
);
|
|
// Only Preliminary Finals → Grand Final actually halves.
|
|
expect(fed.map(([key]) => key)).toEqual(["Grand Final#1"]);
|
|
});
|
|
|
|
it("leaves a bye round's slots seeded rather than inventing feeds", () => {
|
|
// CFP's First Round (4) feeds the Quarterfinals (4) — the top seeds have byes.
|
|
const feeders = buildFeederMap(BRACKET_TEMPLATES.cfp_12);
|
|
expect(feeders.get(matchKey("Quarterfinals", 1))).toEqual([
|
|
{ kind: "seed" },
|
|
{ kind: "seed" },
|
|
]);
|
|
});
|
|
|
|
it("leaves the First Four out of the Round of 64", () => {
|
|
// advanceFirstFourWinner puts each winner in a specific seed slot, not games 1-2.
|
|
const feeders = buildFeederMap(BRACKET_TEMPLATES.ncaa_68);
|
|
expect(feeders.get(matchKey("Round of 64", 1))).toEqual([
|
|
{ kind: "seed" },
|
|
{ kind: "seed" },
|
|
]);
|
|
});
|
|
|
|
it("leaves the NBA play-in alone, where a loser feeds forward", () => {
|
|
// Play-In Round 2 pairs the 7v8 loser with the 9v10 winner, so the round sizes
|
|
// halve but the winners-only rule still doesn't describe it.
|
|
const feeders = buildFeederMap(BRACKET_TEMPLATES.nba_20);
|
|
expect(feeders.get(matchKey("Play-In Round 2", 1))).toEqual([
|
|
{ kind: "seed" },
|
|
{ kind: "seed" },
|
|
]);
|
|
});
|
|
|
|
it("does not route the FIFA final through the third place game", () => {
|
|
// Third Place Game sits between Semifinals and Finals in round order, so array
|
|
// order made it the Finals' feeder and left the Finals' second slot empty.
|
|
const feeders = buildFeederMap(BRACKET_TEMPLATES.fifa_48);
|
|
expect(feeders.get(matchKey("Finals", 1))).toEqual([
|
|
{ kind: "match", ref: { round: "Semifinals", matchNumber: 1 }, result: "winner" },
|
|
{ kind: "match", ref: { round: "Semifinals", matchNumber: 2 }, result: "winner" },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("computeGroupLayout — every template still draws connectors", () => {
|
|
/** Lay a whole template out the way BracketTreeView would. */
|
|
function layOut(template: BracketTemplate) {
|
|
const byRound = allMatches(template);
|
|
const order = template.rounds.map((r) => r.name);
|
|
// BracketTreeView renders a third place game outside the tree.
|
|
const rounds = order.filter((r) => r !== "Third Place Game");
|
|
return computeGroupLayout(rounds, byRound, buildFeederMap(template), order);
|
|
}
|
|
|
|
// A gutter joining a column to one exactly half its size is a plain bracket join and
|
|
// must always be drawn. Where the sizes don't halve — a bye round, a play-in, the
|
|
// First Four — the routing is bespoke and nothing is drawn until the games decide it,
|
|
// which is what these brackets did before.
|
|
it.each(Object.keys(BRACKET_TEMPLATES).filter((id) => id !== "llws_20"))(
|
|
"%s draws every gutter that halves",
|
|
(id) => {
|
|
const layout = layOut(BRACKET_TEMPLATES[id]);
|
|
const gutters = new Set(layout.edges.map((e) => e.fromColumn));
|
|
let halvingGutters = 0;
|
|
for (let ci = 0; ci < layout.columns.length - 1; ci++) {
|
|
const from = layout.columns[ci].matches.length;
|
|
const to = layout.columns[ci + 1].matches.length;
|
|
if (from !== to * 2) continue;
|
|
halvingGutters += 1;
|
|
expect(gutters).toContain(ci);
|
|
}
|
|
// Every template has at least one, so a template that lost all its lines fails.
|
|
expect(halvingGutters).toBeGreaterThan(0);
|
|
}
|
|
);
|
|
|
|
it("keeps the FIFA bracket a single tree once the third place game is set aside", () => {
|
|
const layout = layOut(BRACKET_TEMPLATES.fifa_48);
|
|
expect(layout.columns.map((c) => c.label)).toEqual([
|
|
"Round of 32",
|
|
"Round of 16",
|
|
"Quarterfinals",
|
|
"Semifinals",
|
|
"Finals",
|
|
]);
|
|
expect(layout.edges).toHaveLength(30);
|
|
});
|
|
|
|
// llws_20 is excluded above because both sides in one group is genuinely not a tree;
|
|
// it renders per side, which the tests further up cover.
|
|
});
|
|
|
|
describe("computeGroupLayout — fallback keeps the old connectors", () => {
|
|
const rounds = ["Quarterfinals", "Semifinals", "Finals"];
|
|
const byRound = new Map<string, TestMatch[]>([
|
|
["Quarterfinals", [1, 2, 3, 4].map((n) => ({ round: "Quarterfinals", matchNumber: n }))],
|
|
["Semifinals", [1, 2].map((n) => ({ round: "Semifinals", matchNumber: n }))],
|
|
["Finals", [{ round: "Finals", matchNumber: 1 }]],
|
|
]);
|
|
|
|
it("infers halving edges when there is no feeder map at all", () => {
|
|
// A bracket with no template id, which SportSeasonDisplay renders.
|
|
const layout = computeGroupLayout(rounds, byRound, new Map(), rounds);
|
|
expect(layout.edges).toHaveLength(6);
|
|
// Quarterfinals 1 and 2 both join Semifinal 1.
|
|
const intoFirstSemi = layout.edges.filter((e) => e.toCenter === 1);
|
|
expect(intoFirstSemi.map((e) => e.fromCenter)).toEqual([0.5, 1.5]);
|
|
});
|
|
|
|
it("traces played winners when the shape is not a halving", () => {
|
|
const irregular = new Map<string, TestMatch[]>([
|
|
[
|
|
"Wildcard",
|
|
[
|
|
{ round: "Wildcard", matchNumber: 1, winnerId: "a" },
|
|
{ round: "Wildcard", matchNumber: 2, winnerId: "b" },
|
|
],
|
|
],
|
|
[
|
|
"Semifinals",
|
|
[
|
|
{ round: "Semifinals", matchNumber: 1, participant1Id: "seeded", participant2Id: "b" },
|
|
{ round: "Semifinals", matchNumber: 2, participant1Id: "seeded2", participant2Id: "a" },
|
|
],
|
|
],
|
|
]);
|
|
const layout = computeGroupLayout(
|
|
["Wildcard", "Semifinals"],
|
|
irregular,
|
|
new Map(),
|
|
["Wildcard", "Semifinals"]
|
|
);
|
|
// b won Wildcard 2 (centre 1.5) and plays Semifinal 1 (centre 0.5) — a crossing
|
|
// edge that only the actual result can reveal.
|
|
expect(layout.edges).toContainEqual({ fromColumn: 0, fromCenter: 1.5, toCenter: 0.5 });
|
|
expect(layout.edges).toContainEqual({ fromColumn: 0, fromCenter: 0.5, toCenter: 1.5 });
|
|
});
|
|
});
|
|
|
|
describe("describeSlotSource", () => {
|
|
const feeders = buildFeederMap(LLWS_20);
|
|
const sourcesFor = (game: number): [SlotSource, SlotSource] => {
|
|
const m = GAME_TO_MATCH[game];
|
|
const pair = feeders.get(matchKey(m.round, m.matchNumber));
|
|
if (!pair) throw new Error(`No feeders for G${game}`);
|
|
return pair;
|
|
};
|
|
|
|
it("names a winner feed", () => {
|
|
// G18 = W5 v W9; G5 is Opening Round match 3 on the International side.
|
|
expect(describeSlotSource(sourcesFor(18)[0], LLWS_20)).toBe("Winner of Opening 3");
|
|
});
|
|
|
|
it("names a loser feed, which is the one no line can show", () => {
|
|
// G21 = L9 v W13; G9 is Winners Round 2 match 1 on the International side.
|
|
expect(describeSlotSource(sourcesFor(21)[0], LLWS_20)).toBe("Loser of Winners R2 1");
|
|
// G25 = L18 v W23; G18 is International semifinal 1.
|
|
expect(describeSlotSource(sourcesFor(25)[0], LLWS_20)).toBe("Loser of Winners SF 1");
|
|
});
|
|
|
|
it("uses side-local numbers, as the printed bracket does", () => {
|
|
// G27 = L20 v W21. G20 is Winners Semifinals match 4 globally, but International
|
|
// semifinal 2 — the number the printed bracket uses.
|
|
expect(describeSlotSource(sourcesFor(27)[0], LLWS_20)).toBe("Loser of Winners SF 2");
|
|
});
|
|
|
|
it("names the side where each side plays only one such game", () => {
|
|
// G37 = L36 v L35: both feeds are Bracket Championship losers, one per side, so a
|
|
// number would say nothing and the side is the only thing that tells them apart.
|
|
const [p1, p2] = sourcesFor(37);
|
|
expect(describeSlotSource(p1, LLWS_20)).toBe("Loser of U.S. Bracket Final");
|
|
expect(describeSlotSource(p2, LLWS_20)).toBe("Loser of Intl Bracket Final");
|
|
|
|
// Same rule inside a side bracket: G34 = L30 v W32.
|
|
const [elimP1, elimP2] = sourcesFor(34);
|
|
expect(describeSlotSource(elimP1, LLWS_20)).toBe("Loser of U.S. Winners Final");
|
|
expect(describeSlotSource(elimP2, LLWS_20)).toBe("Winner of U.S. Elim R4");
|
|
});
|
|
|
|
it("drops both number and side for the shared final games", () => {
|
|
// The two sides meet here, so there is only one of each game in the whole bracket.
|
|
const wc = describeSlotSource(
|
|
{ kind: "match", ref: GAME_TO_MATCH[38], result: "winner" },
|
|
LLWS_20
|
|
);
|
|
expect(wc).toBe("Winner of World Championship");
|
|
});
|
|
|
|
it("returns null for a seeded slot so the caller can render TBD", () => {
|
|
expect(describeSlotSource({ kind: "seed" }, LLWS_20)).toBeNull();
|
|
expect(describeSlotSource(undefined, LLWS_20)).toBeNull();
|
|
});
|
|
|
|
it("uses plain round names for non-LLWS templates", () => {
|
|
const template = getBracketTemplate("simple_16");
|
|
const source: SlotSource = {
|
|
kind: "match",
|
|
ref: { round: "Quarterfinals", matchNumber: 3 },
|
|
result: "winner",
|
|
};
|
|
expect(describeSlotSource(source, template)).toBe("Winner of Quarterfinals 3");
|
|
});
|
|
});
|