sendou.ink/app/features/tournament-bracket/components/Bracket/Bracket.browser.test.tsx
2026-07-23 18:48:51 +03:00

1246 lines
29 KiB
TypeScript

import { createMemoryRouter, RouterProvider } from "react-router";
import { describe, expect, test, vi } from "vitest";
import { render } from "vitest-browser-react";
import styles from "~/features/tournament-bracket/components/Bracket/bracket.module.css";
import type { TournamentManagerDataSet } from "~/features/tournament-bracket/core/engine/types";
import type { Bracket as BracketType } from "../../core/Bracket";
import { EliminationBracketSide } from "./Elimination";
import { Bracket } from "./index";
import { RoundRobinBracket } from "./RoundRobin";
import { SwissBracket } from "./Swiss";
const mockTournament = {
ctx: {
id: 1,
name: "Test Tournament",
isFinalized: 0,
castedMatchesInfo: null,
teams: [
{
id: 1,
name: "Team Alpha",
seed: 1,
members: [{ userId: 1, username: "Player1" }],
droppedOut: 0,
},
{
id: 2,
name: "Team Beta",
seed: 2,
members: [{ userId: 2, username: "Player2" }],
droppedOut: 0,
},
{
id: 3,
name: "Team Gamma",
seed: 3,
members: [{ userId: 3, username: "Player3" }],
droppedOut: 0,
},
{
id: 4,
name: "Team Delta",
seed: 4,
members: [{ userId: 4, username: "Player4" }],
droppedOut: 0,
},
{
id: 5,
name: "Team Epsilon",
seed: 5,
members: [{ userId: 5, username: "Player5" }],
droppedOut: 0,
},
{
id: 6,
name: "Team Zeta",
seed: 6,
members: [{ userId: 6, username: "Player6" }],
droppedOut: 0,
},
{
id: 7,
name: "Team Eta",
seed: 7,
members: [{ userId: 7, username: "Player7" }],
droppedOut: 0,
},
{
id: 8,
name: "Team Theta",
seed: 8,
members: [{ userId: 8, username: "Player8" }],
droppedOut: 0,
},
],
settings: {
bracketProgression: [
{
name: "Main Bracket",
type: "double_elimination",
requiresCheckIn: false,
settings: {},
},
],
},
bracketProgressionOverrides: [],
participatedUsers: [1, 2, 3, 4, 5, 6, 7, 8],
},
brackets: [],
teamById: (id: number) =>
mockTournament.ctx.teams.find((t) => t.id === id) ?? null,
teamMemberOfByUser: () => null,
isOrganizer: () => false,
tournamentTeamLogoSrc: () => null,
};
vi.mock("~/features/auth/core/user", () => ({
useUser: () => null,
}));
vi.mock("~/features/tournament/routes/to.$id", () => ({
useTournament: () => mockTournament,
useTournamentVods: () => [],
useBracketExpanded: () => ({
bracketExpanded: true,
setBracketExpanded: vi.fn(),
}),
useStreamingParticipants: () => [],
}));
function createSingleEliminationData(): TournamentManagerDataSet {
return {
stage: [
{
id: 1,
name: "Main Bracket",
number: 1,
type: "single_elimination",
tournament_id: 1,
settings: {},
},
],
group: [{ id: 1, number: 1, stage_id: 1 }],
round: [
{
id: 1,
group_id: 1,
number: 1,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 2,
group_id: 1,
number: 2,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 3,
group_id: 1,
number: 3,
stage_id: 1,
maps: { count: 5, type: "BEST_OF", pickBan: null },
},
],
match: [
{
id: 1,
number: 1,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 1, score: 2, result: "win" },
opponent2: { id: 8, score: 0, result: "loss" },
},
{
id: 2,
number: 2,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 4, score: 2, result: "win" },
opponent2: { id: 5, score: 1, result: "loss" },
},
{
id: 3,
number: 3,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 3, score: 0, result: "loss" },
opponent2: { id: 6, score: 2, result: "win" },
},
{
id: 4,
number: 4,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 2, score: 2, result: "win" },
opponent2: { id: 7, score: 0, result: "loss" },
},
{
id: 5,
number: 1,
stage_id: 1,
group_id: 1,
round_id: 2,
status: 2,
opponent1: { id: 1, score: 2, result: "win" },
opponent2: { id: 4, score: 1, result: "loss" },
},
{
id: 6,
number: 2,
stage_id: 1,
group_id: 1,
round_id: 2,
status: 2,
opponent1: { id: 6, score: 1, result: "loss" },
opponent2: { id: 2, score: 2, result: "win" },
},
{
id: 7,
number: 1,
stage_id: 1,
group_id: 1,
round_id: 3,
status: 4,
opponent1: { id: 1 },
opponent2: { id: 2 },
},
],
};
}
function createByeHeavySingleEliminationData(): TournamentManagerDataSet {
return {
stage: [
{
id: 1,
name: "Main Bracket",
number: 1,
type: "single_elimination",
tournament_id: 1,
settings: {},
},
],
group: [{ id: 1, number: 1, stage_id: 1 }],
round: [
{
id: 1,
group_id: 1,
number: 1,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 2,
group_id: 1,
number: 2,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 3,
group_id: 1,
number: 3,
stage_id: 1,
maps: { count: 5, type: "BEST_OF", pickBan: null },
},
],
match: [
// Round 1 - 5 teams in a bracket of 8, only one match is played
{
id: 1,
number: 1,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 1 },
opponent2: null,
},
{
id: 2,
number: 2,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 3,
opponent1: { id: 4, score: 1 },
opponent2: { id: 5, score: 1 },
},
{
id: 3,
number: 3,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 2 },
opponent2: null,
},
{
id: 4,
number: 4,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 3 },
opponent2: null,
},
// Round 2 - Semis
{
id: 5,
number: 1,
stage_id: 1,
group_id: 1,
round_id: 2,
status: 4,
opponent1: { id: 1 },
opponent2: { id: null },
},
{
id: 6,
number: 2,
stage_id: 1,
group_id: 1,
round_id: 2,
status: 4,
opponent1: { id: 2 },
opponent2: { id: 3 },
},
// Round 3 - Finals
{
id: 7,
number: 1,
stage_id: 1,
group_id: 1,
round_id: 3,
status: 4,
opponent1: { id: null },
opponent2: { id: null },
},
],
};
}
function createDoubleEliminationData(): TournamentManagerDataSet {
return {
stage: [
{
id: 1,
name: "Main Bracket",
number: 1,
type: "double_elimination",
tournament_id: 1,
settings: {},
},
],
group: [
{ id: 1, number: 1, stage_id: 1 },
{ id: 2, number: 2, stage_id: 1 },
],
round: [
{
id: 1,
group_id: 1,
number: 1,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 2,
group_id: 1,
number: 2,
stage_id: 1,
maps: { count: 5, type: "BEST_OF", pickBan: null },
},
{
id: 3,
group_id: 2,
number: 1,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 4,
group_id: 2,
number: 2,
stage_id: 1,
maps: { count: 5, type: "BEST_OF", pickBan: null },
},
],
match: [
{
id: 1,
number: 1,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 1, score: 2, result: "win" },
opponent2: { id: 4, score: 0, result: "loss" },
},
{
id: 2,
number: 2,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 2, score: 2, result: "win" },
opponent2: { id: 3, score: 1, result: "loss" },
},
{
id: 3,
number: 1,
stage_id: 1,
group_id: 1,
round_id: 2,
status: 4,
opponent1: { id: 1 },
opponent2: { id: 2 },
},
{
id: 4,
number: 1,
stage_id: 1,
group_id: 2,
round_id: 3,
status: 2,
opponent1: { id: 4, score: 1, result: "loss" },
opponent2: { id: 3, score: 2, result: "win" },
},
{
id: 5,
number: 1,
stage_id: 1,
group_id: 2,
round_id: 4,
status: 4,
opponent1: { id: 3 },
opponent2: { id: null },
},
],
};
}
function createRoundRobinData(): TournamentManagerDataSet {
return {
stage: [
{
id: 1,
name: "Group Stage",
number: 1,
type: "round_robin",
tournament_id: 1,
settings: { groupCount: 2 },
},
],
group: [
{ id: 1, number: 1, stage_id: 1 },
{ id: 2, number: 2, stage_id: 1 },
],
round: [
{
id: 1,
group_id: 1,
number: 1,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 2,
group_id: 1,
number: 2,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 3,
group_id: 1,
number: 3,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 4,
group_id: 2,
number: 1,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 5,
group_id: 2,
number: 2,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 6,
group_id: 2,
number: 3,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
],
match: [
{
id: 1,
number: 1,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 1, score: 2, result: "win" },
opponent2: { id: 2, score: 0, result: "loss" },
},
{
id: 2,
number: 2,
stage_id: 1,
group_id: 1,
round_id: 2,
status: 2,
opponent1: { id: 1, score: 2, result: "win" },
opponent2: { id: 3, score: 1, result: "loss" },
},
{
id: 3,
number: 3,
stage_id: 1,
group_id: 1,
round_id: 3,
status: 4,
opponent1: { id: 2 },
opponent2: { id: 3 },
},
{
id: 4,
number: 1,
stage_id: 1,
group_id: 2,
round_id: 4,
status: 2,
opponent1: { id: 4, score: 2, result: "win" },
opponent2: { id: 5, score: 1, result: "loss" },
},
{
id: 5,
number: 2,
stage_id: 1,
group_id: 2,
round_id: 5,
status: 4,
opponent1: { id: 4 },
opponent2: { id: 6 },
},
{
id: 6,
number: 3,
stage_id: 1,
group_id: 2,
round_id: 6,
status: 4,
opponent1: { id: 5 },
opponent2: { id: 6 },
},
],
};
}
function createSwissData(): TournamentManagerDataSet {
return {
stage: [
{
id: 1,
name: "Swiss Stage",
number: 1,
type: "swiss",
tournament_id: 1,
settings: { groupCount: 1 },
},
],
group: [{ id: 1, number: 1, stage_id: 1 }],
round: [
{
id: 1,
group_id: 1,
number: 1,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 2,
group_id: 1,
number: 2,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 3,
group_id: 1,
number: 3,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
],
match: [
{
id: 1,
number: 1,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 1, score: 2, result: "win" },
opponent2: { id: 8, score: 0, result: "loss" },
},
{
id: 2,
number: 2,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 2, score: 2, result: "win" },
opponent2: { id: 7, score: 1, result: "loss" },
},
{
id: 3,
number: 3,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 3, score: 1, result: "loss" },
opponent2: { id: 6, score: 2, result: "win" },
},
{
id: 4,
number: 4,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 4, score: 0, result: "loss" },
opponent2: { id: 5, score: 2, result: "win" },
},
{
id: 5,
number: 1,
stage_id: 1,
group_id: 1,
round_id: 2,
status: 4,
opponent1: { id: 1 },
opponent2: { id: 2 },
},
{
id: 6,
number: 2,
stage_id: 1,
group_id: 1,
round_id: 2,
status: 4,
opponent1: { id: 5 },
opponent2: { id: 6 },
},
],
};
}
function createLargeSingleEliminationData(options?: {
ongoingRoundIdx?: number;
}): TournamentManagerDataSet {
const { ongoingRoundIdx } = options ?? {};
return {
stage: [
{
id: 1,
name: "Main Bracket",
number: 1,
type: "single_elimination",
tournament_id: 1,
settings: {},
},
],
group: [{ id: 1, number: 1, stage_id: 1 }],
round: [
{
id: 1,
group_id: 1,
number: 1,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 2,
group_id: 1,
number: 2,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 3,
group_id: 1,
number: 3,
stage_id: 1,
maps: { count: 3, type: "BEST_OF", pickBan: null },
},
{
id: 4,
group_id: 1,
number: 4,
stage_id: 1,
maps: { count: 5, type: "BEST_OF", pickBan: null },
},
],
match: [
// Round 1 - 8 matches (all completed unless ongoingRoundIdx === 0)
{
id: 1,
number: 1,
stage_id: 1,
group_id: 1,
round_id: 1,
status: ongoingRoundIdx === 0 ? 3 : 2,
opponent1:
ongoingRoundIdx === 0
? { id: 1, score: 1 }
: { id: 1, score: 2, result: "win" },
opponent2:
ongoingRoundIdx === 0
? { id: 8, score: 1 }
: { id: 8, score: 0, result: "loss" },
},
{
id: 2,
number: 2,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 2, score: 2, result: "win" },
opponent2: { id: 7, score: 0, result: "loss" },
},
{
id: 3,
number: 3,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 3, score: 2, result: "win" },
opponent2: { id: 6, score: 1, result: "loss" },
},
{
id: 4,
number: 4,
stage_id: 1,
group_id: 1,
round_id: 1,
status: 2,
opponent1: { id: 4, score: 2, result: "win" },
opponent2: { id: 5, score: 0, result: "loss" },
},
// Round 2 - 4 matches (all completed unless ongoingRoundIdx === 1)
{
id: 5,
number: 1,
stage_id: 1,
group_id: 1,
round_id: 2,
status: ongoingRoundIdx === 1 ? 3 : 2,
opponent1:
ongoingRoundIdx === 1
? { id: 1, score: 1 }
: { id: 1, score: 2, result: "win" },
opponent2:
ongoingRoundIdx === 1
? { id: 2, score: 1 }
: { id: 2, score: 1, result: "loss" },
},
{
id: 6,
number: 2,
stage_id: 1,
group_id: 1,
round_id: 2,
status: 2,
opponent1: { id: 3, score: 1, result: "loss" },
opponent2: { id: 4, score: 2, result: "win" },
},
// Round 3 - Semifinals (completed unless ongoingRoundIdx === 2)
{
id: 7,
number: 1,
stage_id: 1,
group_id: 1,
round_id: 3,
status: ongoingRoundIdx === 2 ? 3 : 2,
opponent1:
ongoingRoundIdx === 2
? { id: 1, score: 1 }
: { id: 1, score: 2, result: "win" },
opponent2:
ongoingRoundIdx === 2
? { id: 4, score: 1 }
: { id: 4, score: 0, result: "loss" },
},
// Round 4 - Finals (ongoing by default)
{
id: 8,
number: 1,
stage_id: 1,
group_id: 1,
round_id: 4,
status: 4,
opponent1: { id: 1 },
opponent2: { id: 4 },
},
],
};
}
function createMockBracket(
type: "single_elimination" | "double_elimination" | "round_robin" | "swiss",
data: TournamentManagerDataSet,
): BracketType {
return {
id: 1,
idx: 0,
preview: false,
data,
type,
name: "Main Bracket",
canBeStarted: false,
tournament: mockTournament as any,
settings: type === "swiss" ? { roundCount: 3 } : null,
sources: undefined,
seeding: undefined,
createdAt: null,
requiresCheckIn: false,
startTime: null,
simulatedMatch: () => undefined,
currentStandings: () => [],
participantTournamentTeamIds: [1, 2, 3, 4, 5, 6, 7, 8],
} as unknown as BracketType;
}
function renderWithRouter(element: React.ReactNode) {
const router = createMemoryRouter([{ path: "/", element }], {
initialEntries: ["/"],
});
return render(<RouterProvider router={router} />);
}
describe("Single Elimination Bracket", () => {
test("renders single elimination bracket with rounds", async () => {
const data = createSingleEliminationData();
const bracket = createMockBracket("single_elimination", data);
const screen = await renderWithRouter(
<EliminationBracketSide bracket={bracket} type="single" isExpanded />,
);
// 8-team bracket has Round 1, Semis, Finals
await expect.element(screen.getByText("Round 1")).toBeVisible();
await expect.element(screen.getByText("Semis")).toBeVisible();
await expect.element(screen.getByText("Finals")).toBeVisible();
});
test("renders team names in matches", async () => {
const data = createSingleEliminationData();
const bracket = createMockBracket("single_elimination", data);
const screen = await renderWithRouter(
<EliminationBracketSide bracket={bracket} type="single" isExpanded />,
);
await expect.element(screen.getByText("Team Alpha").first()).toBeVisible();
await expect.element(screen.getByText("Team Beta").first()).toBeVisible();
await expect.element(screen.getByText("Team Gamma").first()).toBeVisible();
await expect.element(screen.getByText("Team Delta").first()).toBeVisible();
});
test("renders match scores", async () => {
const data = createSingleEliminationData();
const bracket = createMockBracket("single_elimination", data);
const screen = await renderWithRouter(
<EliminationBracketSide bracket={bracket} type="single" isExpanded />,
);
const scores = screen.container.querySelectorAll(`.${styles.matchScore}`);
expect(scores.length).toBeGreaterThan(0);
});
test("renders match identifiers with round and number", async () => {
const data = createSingleEliminationData();
const bracket = createMockBracket("single_elimination", data);
const screen = await renderWithRouter(
<EliminationBracketSide bracket={bracket} type="single" isExpanded />,
);
await expect.element(screen.getByText("1.1")).toBeVisible();
await expect.element(screen.getByText("1.2")).toBeVisible();
});
test("hides early completed rounds when isExpanded is false", async () => {
const data = createLargeSingleEliminationData();
const bracket = createMockBracket("single_elimination", data);
const screen = await renderWithRouter(
<EliminationBracketSide
bracket={bracket}
type="single"
isExpanded={false}
/>,
);
// Round 1 and Round 2 should be hidden (completed, not in last 2)
const round1Elements = screen.container.querySelectorAll(
'[data-round-id="1"]',
);
const round2Elements = screen.container.querySelectorAll(
'[data-round-id="2"]',
);
expect(round1Elements.length).toBe(0);
expect(round2Elements.length).toBe(0);
// Semis and Finals should be visible (last 2 rounds)
await expect.element(screen.getByText("Semis")).toBeVisible();
await expect.element(screen.getByText("Finals")).toBeVisible();
});
test("always shows at least last 2 rounds when isExpanded is false", async () => {
const data = createLargeSingleEliminationData();
const bracket = createMockBracket("single_elimination", data);
const screen = await renderWithRouter(
<EliminationBracketSide
bracket={bracket}
type="single"
isExpanded={false}
/>,
);
// Should show exactly 2 round columns (Semifinals and Finals)
const roundColumns = screen.container.querySelectorAll(
`.${styles.elimRoundColumn}`,
);
expect(roundColumns.length).toBe(2);
});
test("shows all rounds when isExpanded is true", async () => {
const data = createLargeSingleEliminationData();
const bracket = createMockBracket("single_elimination", data);
const screen = await renderWithRouter(
<EliminationBracketSide bracket={bracket} type="single" isExpanded />,
);
// All 4 rounds should be visible
await expect.element(screen.getByText("Round 1")).toBeVisible();
await expect.element(screen.getByText("Round 2")).toBeVisible();
await expect.element(screen.getByText("Semis")).toBeVisible();
await expect.element(screen.getByText("Finals")).toBeVisible();
});
test("compacts first round when fewer than half of its matches are played", async () => {
const data = createByeHeavySingleEliminationData();
const bracket = createMockBracket("single_elimination", data);
const screen = await renderWithRouter(
<EliminationBracketSide bracket={bracket} type="single" isExpanded />,
);
// one slot per round 2 match instead of one per potential round 1 match
const firstRoundMatchWrappers = screen.container.querySelectorAll(
`[data-round-id="1"] .${styles.matchWrapper}`,
);
expect(firstRoundMatchWrappers.length).toBe(2);
// played match connects to its destination with a straight line
const straightLines = screen.container.querySelectorAll(
`[data-round-id="1"] .${styles.matchLineStraight}`,
);
expect(straightLines.length).toBeGreaterThan(0);
await expect.element(screen.getByText("Team Delta")).toBeVisible();
await expect.element(screen.getByText("Team Epsilon")).toBeVisible();
});
test("does not compact first round when at least half of its matches are played", async () => {
const data = createSingleEliminationData();
const bracket = createMockBracket("single_elimination", data);
const screen = await renderWithRouter(
<EliminationBracketSide bracket={bracket} type="single" isExpanded />,
);
const firstRoundMatchWrappers = screen.container.querySelectorAll(
`[data-round-id="1"] .${styles.matchWrapper}`,
);
expect(firstRoundMatchWrappers.length).toBe(4);
});
test("shows early round with ongoing match even when isExpanded is false", async () => {
const data = createLargeSingleEliminationData({ ongoingRoundIdx: 0 });
const bracket = createMockBracket("single_elimination", data);
const screen = await renderWithRouter(
<EliminationBracketSide
bracket={bracket}
type="single"
isExpanded={false}
/>,
);
// Round 1 should be visible because it has an ongoing match
await expect.element(screen.getByText("Round 1")).toBeVisible();
});
});
describe("Double Elimination Bracket", () => {
test("renders winners bracket side", async () => {
const data = createDoubleEliminationData();
const bracket = createMockBracket("double_elimination", data);
const screen = await renderWithRouter(
<EliminationBracketSide bracket={bracket} type="winners" isExpanded />,
);
// Small 4-team bracket has Grand Finals and Bracket Reset rounds
await expect.element(screen.getByText("Grand Finals")).toBeVisible();
});
test("renders losers bracket side", async () => {
const data = createDoubleEliminationData();
const bracket = createMockBracket("double_elimination", data);
const screen = await renderWithRouter(
<EliminationBracketSide bracket={bracket} type="losers" isExpanded />,
);
// Small 4-team losers bracket has LB Semis and LB Finals
await expect.element(screen.getByText("LB Semis")).toBeVisible();
});
test("renders team names in winners bracket", async () => {
const data = createDoubleEliminationData();
const bracket = createMockBracket("double_elimination", data);
const screen = await renderWithRouter(
<EliminationBracketSide bracket={bracket} type="winners" isExpanded />,
);
await expect.element(screen.getByText("Team Alpha")).toBeVisible();
await expect.element(screen.getByText("Team Beta")).toBeVisible();
});
test("renders match headers with GF prefix for grand finals", async () => {
const data = createDoubleEliminationData();
const bracket = createMockBracket("double_elimination", data);
const screen = await renderWithRouter(
<EliminationBracketSide bracket={bracket} type="winners" isExpanded />,
);
// Small 4-team bracket only has Grand Finals (GF prefix), not regular WB rounds
const headerBox = screen.container.querySelector(
`.${styles.matchHeaderBox}`,
);
expect(headerBox?.textContent).toContain("GF");
expect(headerBox?.textContent).toContain("1.1");
});
});
describe("Round Robin Bracket", () => {
test("renders group headers", async () => {
const data = createRoundRobinData();
const bracket = createMockBracket("round_robin", data);
const screen = await renderWithRouter(
<RoundRobinBracket bracket={bracket} />,
);
await expect.element(screen.getByText("Group A")).toBeVisible();
await expect.element(screen.getByText("Group B")).toBeVisible();
});
test("renders round headers within groups", async () => {
const data = createRoundRobinData();
const bracket = createMockBracket("round_robin", data);
const screen = await renderWithRouter(
<RoundRobinBracket bracket={bracket} />,
);
const round1Headers = screen.getByText("Round 1");
await expect.element(round1Headers.first()).toBeVisible();
});
test("renders teams in group matches", async () => {
const data = createRoundRobinData();
const bracket = createMockBracket("round_robin", data);
const screen = await renderWithRouter(
<RoundRobinBracket bracket={bracket} />,
);
await expect.element(screen.getByText("Team Alpha").first()).toBeVisible();
await expect.element(screen.getByText("Team Beta").first()).toBeVisible();
await expect.element(screen.getByText("Team Delta").first()).toBeVisible();
});
test("renders match identifiers with group prefix", async () => {
const data = createRoundRobinData();
const bracket = createMockBracket("round_robin", data);
const screen = await renderWithRouter(
<RoundRobinBracket bracket={bracket} />,
);
await expect.element(screen.getByText(/A1\.1/)).toBeVisible();
await expect.element(screen.getByText(/B1\.1/)).toBeVisible();
});
test("renders placements table for each group", async () => {
const data = createRoundRobinData();
const bracket = createMockBracket("round_robin", data);
const screen = await renderWithRouter(
<RoundRobinBracket bracket={bracket} />,
);
const tables = screen.container.querySelectorAll(
`.${styles.rrPlacementsTable}`,
);
expect(tables.length).toBe(2);
});
});
describe("Swiss Bracket", () => {
test("renders round headers", async () => {
const data = createSwissData();
const bracket = createMockBracket("swiss", data);
const screen = await renderWithRouter(
<SwissBracket bracket={bracket} bracketIdx={0} />,
);
await expect.element(screen.getByText("Round 1")).toBeVisible();
await expect.element(screen.getByText("Round 2")).toBeVisible();
});
test("renders team names in matches", async () => {
const data = createSwissData();
const bracket = createMockBracket("swiss", data);
const screen = await renderWithRouter(
<SwissBracket bracket={bracket} bracketIdx={0} />,
);
await expect.element(screen.getByText("Team Alpha").first()).toBeVisible();
await expect.element(screen.getByText("Team Beta").first()).toBeVisible();
});
test("renders completed match scores", async () => {
const data = createSwissData();
const bracket = createMockBracket("swiss", data);
const screen = await renderWithRouter(
<SwissBracket bracket={bracket} bracketIdx={0} />,
);
const scores = screen.container.querySelectorAll(`.${styles.matchScore}`);
expect(scores.length).toBeGreaterThan(0);
});
test("renders placements table", async () => {
const data = createSwissData();
const bracket = createMockBracket("swiss", data);
const screen = await renderWithRouter(
<SwissBracket bracket={bracket} bracketIdx={0} />,
);
const table = screen.container.querySelector(
`.${styles.rrPlacementsTable}`,
);
expect(table).not.toBeNull();
});
test("renders match identifiers with group prefix", async () => {
const data = createSwissData();
const bracket = createMockBracket("swiss", data);
const screen = await renderWithRouter(
<SwissBracket bracket={bracket} bracketIdx={0} />,
);
await expect.element(screen.getByText(/A1\.1/)).toBeVisible();
});
});
describe("Bracket container component", () => {
test("renders single elimination through main Bracket component", async () => {
const data = createSingleEliminationData();
const bracket = createMockBracket("single_elimination", data);
const screen = await renderWithRouter(
<Bracket bracket={bracket} bracketIdx={0} />,
);
await expect.element(screen.getByTestId("brackets-viewer")).toBeVisible();
});
test("renders double elimination through main Bracket component", async () => {
const data = createDoubleEliminationData();
const bracket = createMockBracket("double_elimination", data);
const screen = await renderWithRouter(
<Bracket bracket={bracket} bracketIdx={0} />,
);
await expect.element(screen.getByTestId("brackets-viewer")).toBeVisible();
});
test("renders round robin through main Bracket component", async () => {
const data = createRoundRobinData();
const bracket = createMockBracket("round_robin", data);
const screen = await renderWithRouter(
<Bracket bracket={bracket} bracketIdx={0} />,
);
await expect.element(screen.getByTestId("brackets-viewer")).toBeVisible();
});
test("renders swiss through main Bracket component", async () => {
const data = createSwissData();
const bracket = createMockBracket("swiss", data);
const screen = await renderWithRouter(
<Bracket bracket={bracket} bracketIdx={0} />,
);
await expect.element(screen.getByTestId("brackets-viewer")).toBeVisible();
});
});