323 lines
9.7 KiB
TypeScript
323 lines
9.7 KiB
TypeScript
|
|
import { describe, it, expect } from "vitest";
|
|||
|
|
import { render, screen } from "@testing-library/react";
|
|||
|
|
import { StandingsTable } from "../StandingsTable";
|
|||
|
|
import { type TeamStanding } from "~/models/standings";
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* StandingsTable Component Tests
|
|||
|
|
* Phase 4.1: Test standings display with tiebreakers and placement breakdown
|
|||
|
|
*/
|
|||
|
|
describe("StandingsTable", () => {
|
|||
|
|
const mockStandings: TeamStanding[] = [
|
|||
|
|
{
|
|||
|
|
teamId: "team1",
|
|||
|
|
teamName: "Champions United",
|
|||
|
|
totalPoints: 250.5,
|
|||
|
|
currentRank: 1,
|
|||
|
|
previousRank: 2,
|
|||
|
|
rankChange: 1, // Moved up
|
|||
|
|
placementCounts: {
|
|||
|
|
first: 2,
|
|||
|
|
second: 1,
|
|||
|
|
third: 0,
|
|||
|
|
fourth: 1,
|
|||
|
|
fifth: 0,
|
|||
|
|
sixth: 0,
|
|||
|
|
seventh: 0,
|
|||
|
|
eighth: 1,
|
|||
|
|
},
|
|||
|
|
participantsRemaining: 3,
|
|||
|
|
calculatedAt: new Date(),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
teamId: "team2",
|
|||
|
|
teamName: "Second Place Squad",
|
|||
|
|
totalPoints: 245.0,
|
|||
|
|
currentRank: 2,
|
|||
|
|
previousRank: 1,
|
|||
|
|
rankChange: -1, // Moved down
|
|||
|
|
placementCounts: {
|
|||
|
|
first: 1,
|
|||
|
|
second: 2,
|
|||
|
|
third: 1,
|
|||
|
|
fourth: 0,
|
|||
|
|
fifth: 1,
|
|||
|
|
sixth: 0,
|
|||
|
|
seventh: 0,
|
|||
|
|
eighth: 0,
|
|||
|
|
},
|
|||
|
|
participantsRemaining: 0,
|
|||
|
|
calculatedAt: new Date(),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
teamId: "team3",
|
|||
|
|
teamName: "Bronze Warriors",
|
|||
|
|
totalPoints: 200.0,
|
|||
|
|
currentRank: 3,
|
|||
|
|
previousRank: 3,
|
|||
|
|
rankChange: 0, // No change
|
|||
|
|
placementCounts: {
|
|||
|
|
first: 0,
|
|||
|
|
second: 1,
|
|||
|
|
third: 2,
|
|||
|
|
fourth: 1,
|
|||
|
|
fifth: 0,
|
|||
|
|
sixth: 0,
|
|||
|
|
seventh: 0,
|
|||
|
|
eighth: 0,
|
|||
|
|
},
|
|||
|
|
participantsRemaining: 5,
|
|||
|
|
calculatedAt: new Date(),
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
describe("Basic Rendering", () => {
|
|||
|
|
it("should render standings table with all teams", () => {
|
|||
|
|
render(<StandingsTable standings={mockStandings} />);
|
|||
|
|
|
|||
|
|
expect(screen.getByText("Champions United")).toBeInTheDocument();
|
|||
|
|
expect(screen.getByText("Second Place Squad")).toBeInTheDocument();
|
|||
|
|
expect(screen.getByText("Bronze Warriors")).toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("should display total points for each team", () => {
|
|||
|
|
render(<StandingsTable standings={mockStandings} />);
|
|||
|
|
|
|||
|
|
expect(screen.getByText("250.5")).toBeInTheDocument();
|
|||
|
|
expect(screen.getByText("245.0")).toBeInTheDocument();
|
|||
|
|
expect(screen.getByText("200.0")).toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("should show empty state when no standings", () => {
|
|||
|
|
render(<StandingsTable standings={[]} />);
|
|||
|
|
|
|||
|
|
expect(screen.getByText("No standings data available")).toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe("Rank Badges", () => {
|
|||
|
|
it("should show trophy icon for 1st place", () => {
|
|||
|
|
render(<StandingsTable standings={mockStandings} />);
|
|||
|
|
|
|||
|
|
expect(screen.getByText(/🏆.*1st/)).toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("should show silver medal icon for 2nd place", () => {
|
|||
|
|
render(<StandingsTable standings={mockStandings} />);
|
|||
|
|
|
|||
|
|
expect(screen.getByText(/🥈.*2nd/)).toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("should show bronze medal icon for 3rd place", () => {
|
|||
|
|
render(<StandingsTable standings={mockStandings} />);
|
|||
|
|
|
|||
|
|
expect(screen.getByText(/🥉.*3rd/)).toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("should show plain rank for 4th place and below", () => {
|
|||
|
|
const standings: TeamStanding[] = [
|
|||
|
|
{
|
|||
|
|
...mockStandings[0],
|
|||
|
|
currentRank: 4,
|
|||
|
|
teamName: "Fourth Place",
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
render(<StandingsTable standings={standings} />);
|
|||
|
|
|
|||
|
|
// Should not have emoji, just the number
|
|||
|
|
const badge = screen.getByText("4");
|
|||
|
|
expect(badge).toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe("Movement Indicators", () => {
|
|||
|
|
it("should show up arrow for positive rank change", () => {
|
|||
|
|
render(<StandingsTable standings={mockStandings} />);
|
|||
|
|
|
|||
|
|
expect(screen.getByText("↑1")).toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("should show down arrow for negative rank change", () => {
|
|||
|
|
render(<StandingsTable standings={mockStandings} />);
|
|||
|
|
|
|||
|
|
expect(screen.getByText("↓1")).toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("should not show indicator for no rank change", () => {
|
|||
|
|
render(<StandingsTable standings={mockStandings} />);
|
|||
|
|
|
|||
|
|
// Third team has no change, so no arrow
|
|||
|
|
const arrows = screen.queryAllByText(/↑|↓/);
|
|||
|
|
expect(arrows).toHaveLength(2); // Only first two teams
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe("Placement Breakdown", () => {
|
|||
|
|
it("should display placement counts", () => {
|
|||
|
|
const singleTeam: TeamStanding[] = [
|
|||
|
|
{
|
|||
|
|
teamId: "team1",
|
|||
|
|
teamName: "Test Team",
|
|||
|
|
totalPoints: 250,
|
|||
|
|
currentRank: 1,
|
|||
|
|
previousRank: null,
|
|||
|
|
rankChange: 0,
|
|||
|
|
placementCounts: {
|
|||
|
|
first: 2,
|
|||
|
|
second: 1,
|
|||
|
|
third: 0,
|
|||
|
|
fourth: 1,
|
|||
|
|
fifth: 0,
|
|||
|
|
sixth: 0,
|
|||
|
|
seventh: 0,
|
|||
|
|
eighth: 1,
|
|||
|
|
},
|
|||
|
|
participantsRemaining: 0,
|
|||
|
|
calculatedAt: new Date(),
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
render(<StandingsTable standings={singleTeam} showPlacementBreakdown />);
|
|||
|
|
|
|||
|
|
// Test Team: 2 firsts, 1 second, 1 fourth, 1 eighth
|
|||
|
|
expect(screen.getByText("1st×2")).toBeInTheDocument();
|
|||
|
|
expect(screen.getByText("2nd×1")).toBeInTheDocument();
|
|||
|
|
expect(screen.getByText("4th×1")).toBeInTheDocument();
|
|||
|
|
expect(screen.getByText("8th×1")).toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("should only show non-zero placement counts", () => {
|
|||
|
|
render(<StandingsTable standings={mockStandings} showPlacementBreakdown />);
|
|||
|
|
|
|||
|
|
// Should not show "3rd×0" for Champions United
|
|||
|
|
const placements = screen.queryByText("3rd×0");
|
|||
|
|
expect(placements).not.toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("should hide placement breakdown when disabled", () => {
|
|||
|
|
render(<StandingsTable standings={mockStandings} showPlacementBreakdown={false} />);
|
|||
|
|
|
|||
|
|
expect(screen.queryByText("1st×2")).not.toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("should show 'None yet' when no placements", () => {
|
|||
|
|
const standings: TeamStanding[] = [
|
|||
|
|
{
|
|||
|
|
...mockStandings[0],
|
|||
|
|
placementCounts: {
|
|||
|
|
first: 0,
|
|||
|
|
second: 0,
|
|||
|
|
third: 0,
|
|||
|
|
fourth: 0,
|
|||
|
|
fifth: 0,
|
|||
|
|
sixth: 0,
|
|||
|
|
seventh: 0,
|
|||
|
|
eighth: 0,
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
render(<StandingsTable standings={standings} showPlacementBreakdown />);
|
|||
|
|
|
|||
|
|
expect(screen.getByText("None yet")).toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe("Participants Remaining", () => {
|
|||
|
|
it("should show remaining count when participants are incomplete", () => {
|
|||
|
|
render(<StandingsTable standings={mockStandings} />);
|
|||
|
|
|
|||
|
|
expect(screen.getByText("3 remaining")).toBeInTheDocument();
|
|||
|
|
expect(screen.getByText("5 remaining")).toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("should show 'Complete' badge when all participants done", () => {
|
|||
|
|
render(<StandingsTable standings={mockStandings} />);
|
|||
|
|
|
|||
|
|
expect(screen.getByText("Complete")).toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe("Table Structure", () => {
|
|||
|
|
it("should have correct table headers", () => {
|
|||
|
|
render(<StandingsTable standings={mockStandings} />);
|
|||
|
|
|
|||
|
|
expect(screen.getByText("Rank")).toBeInTheDocument();
|
|||
|
|
expect(screen.getByText("Team")).toBeInTheDocument();
|
|||
|
|
expect(screen.getByText("Points")).toBeInTheDocument();
|
|||
|
|
expect(screen.getByText("Placements")).toBeInTheDocument();
|
|||
|
|
expect(screen.getByText("Remaining")).toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("should render teams in order", () => {
|
|||
|
|
const { container } = render(<StandingsTable standings={mockStandings} />);
|
|||
|
|
|
|||
|
|
const rows = container.querySelectorAll("tbody tr");
|
|||
|
|
expect(rows).toHaveLength(3);
|
|||
|
|
|
|||
|
|
// First row should be Champions United (rank 1)
|
|||
|
|
expect(rows[0].textContent).toContain("Champions United");
|
|||
|
|
|
|||
|
|
// Second row should be Second Place Squad (rank 2)
|
|||
|
|
expect(rows[1].textContent).toContain("Second Place Squad");
|
|||
|
|
|
|||
|
|
// Third row should be Bronze Warriors (rank 3)
|
|||
|
|
expect(rows[2].textContent).toContain("Bronze Warriors");
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe("Tied Ranks", () => {
|
|||
|
|
it("should handle teams with same rank", () => {
|
|||
|
|
const tiedStandings: TeamStanding[] = [
|
|||
|
|
{
|
|||
|
|
...mockStandings[0],
|
|||
|
|
currentRank: 1,
|
|||
|
|
teamName: "Team A",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
...mockStandings[1],
|
|||
|
|
currentRank: 1,
|
|||
|
|
teamName: "Team B",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
...mockStandings[2],
|
|||
|
|
currentRank: 3, // Skips 2
|
|||
|
|
teamName: "Team C",
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
render(<StandingsTable standings={tiedStandings} />);
|
|||
|
|
|
|||
|
|
// Both teams should show rank 1
|
|||
|
|
const firstPlaceBadges = screen.getAllByText(/🏆.*1st/);
|
|||
|
|
expect(firstPlaceBadges).toHaveLength(2);
|
|||
|
|
|
|||
|
|
// Third team should show rank 3 (not 2)
|
|||
|
|
expect(screen.getByText(/🥉.*3rd/)).toBeInTheDocument();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe("Accessibility", () => {
|
|||
|
|
it("should have title attributes for movement indicators", () => {
|
|||
|
|
const { container } = render(<StandingsTable standings={mockStandings} />);
|
|||
|
|
|
|||
|
|
const upArrow = container.querySelector('[title*="Up"]');
|
|||
|
|
expect(upArrow).toBeInTheDocument();
|
|||
|
|
expect(upArrow?.getAttribute("title")).toContain("Up 1 place");
|
|||
|
|
|
|||
|
|
const downArrow = container.querySelector('[title*="Down"]');
|
|||
|
|
expect(downArrow).toBeInTheDocument();
|
|||
|
|
expect(downArrow?.getAttribute("title")).toContain("Down 1 place");
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("should have title attributes for placement counts", () => {
|
|||
|
|
const { container } = render(<StandingsTable standings={mockStandings} showPlacementBreakdown />);
|
|||
|
|
|
|||
|
|
const firstPlace = container.querySelector('[title*="1st place"]');
|
|||
|
|
expect(firstPlace).toBeInTheDocument();
|
|||
|
|
expect(firstPlace?.getAttribute("title")).toContain("2 1st place finishes");
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|