brackt/app/components/standings/__tests__/StandingsTable.test.tsx
Chris Parsons 0385ca220c feat: add standings page and enhance standings functionality
- Added a new route for league standings: `/leagues/:leagueId/standings/:seasonId`
- Implemented the `StandingsTable` component to display team standings with ranking, points, and placement breakdown.
- Integrated tiebreaker logic for ranking teams based on total points and placements.
- Enhanced the league home page with a link to view standings.
- Updated scoring system documentation to reflect the new standings features.
- Added comprehensive tests for tiebreaker logic and `StandingsTable` component.
- Created shared types for standings to be used in both server and client code.
2025-11-13 13:24:03 -08:00

322 lines
9.7 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 { 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");
});
});
});