diff --git a/app/features/tournament-bracket/components/Bracket/Bracket.browser.test.tsx b/app/features/tournament-bracket/components/Bracket/Bracket.browser.test.tsx
new file mode 100644
index 000000000..43ef716ae
--- /dev/null
+++ b/app/features/tournament-bracket/components/Bracket/Bracket.browser.test.tsx
@@ -0,0 +1,1089 @@
+import { createMemoryRouter, RouterProvider } from "react-router";
+import { describe, expect, test, vi } from "vitest";
+import { render } from "vitest-browser-react";
+import type { TournamentManagerDataSet } from "~/modules/brackets-manager/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";
+import "~/features/tournament-bracket/components/Bracket/bracket.css";
+import "~/features/tournament-bracket/tournament-bracket.css";
+
+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,
+ 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: { size: 8 },
+ },
+ ],
+ 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 createDoubleEliminationData(): TournamentManagerDataSet {
+ return {
+ stage: [
+ {
+ id: 1,
+ name: "Main Bracket",
+ number: 1,
+ type: "double_elimination",
+ tournament_id: 1,
+ settings: { size: 4, grandFinal: "double" },
+ },
+ ],
+ 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, roundRobinMode: "simple", size: 6 },
+ },
+ ],
+ 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: { size: 16 },
+ },
+ ],
+ 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();
+}
+
+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(
+ ,
+ );
+
+ // 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ const scores = screen.container.querySelectorAll(".bracket__match__score");
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ // 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(
+ ,
+ );
+
+ // Should show exactly 2 round columns (Semifinals and Finals)
+ const roundColumns = screen.container.querySelectorAll(
+ ".elim-bracket__round-column",
+ );
+ 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(
+ ,
+ );
+
+ // 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("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(
+ ,
+ );
+
+ // 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(
+ ,
+ );
+
+ // 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(
+ ,
+ );
+
+ // 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ // Small 4-team bracket only has Grand Finals (GF prefix), not regular WB rounds
+ const headerBox = screen.container.querySelector(
+ ".bracket__match__header__box",
+ );
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ const tables = screen.container.querySelectorAll(".rr__placements-table");
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ const scores = screen.container.querySelectorAll(".bracket__match__score");
+ expect(scores.length).toBeGreaterThan(0);
+ });
+
+ test("renders placements table", async () => {
+ const data = createSwissData();
+ const bracket = createMockBracket("swiss", data);
+
+ const screen = await renderWithRouter(
+ ,
+ );
+
+ const table = screen.container.querySelector(".rr__placements-table");
+ expect(table).not.toBeNull();
+ });
+
+ test("renders match identifiers with group prefix", async () => {
+ const data = createSwissData();
+ const bracket = createMockBracket("swiss", data);
+
+ const screen = await renderWithRouter(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ await expect.element(screen.getByTestId("brackets-viewer")).toBeVisible();
+ });
+});
diff --git a/vitest.browser.config.ts b/vitest.browser.config.ts
index 75a9f9d82..b9c66fdbb 100644
--- a/vitest.browser.config.ts
+++ b/vitest.browser.config.ts
@@ -6,6 +6,17 @@ const headless = process.env.BROWSER_HEADLESS === "true";
export default defineConfig({
plugins: [tsconfigPaths()],
+ optimizeDeps: {
+ include: [
+ "react",
+ "react/jsx-runtime",
+ "react/jsx-dev-runtime",
+ "react-dom",
+ "react-dom/client",
+ "react-router",
+ "react-use-draggable-scroll",
+ ],
+ },
test: {
name: "browser",
include: ["**/*.browser.test.{ts,tsx}"],