2025-11-13 13:24:03 -08:00
|
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
|
import { render, screen } from "@testing-library/react";
|
2025-11-14 20:01:21 -08:00
|
|
|
import { BrowserRouter } from "react-router";
|
2025-11-13 13:24:03 -08:00
|
|
|
import { StandingsTable } from "../StandingsTable";
|
|
|
|
|
import { type TeamStanding } from "~/models/standings";
|
|
|
|
|
|
2025-11-14 20:01:21 -08:00
|
|
|
// Helper to wrap component with router context
|
|
|
|
|
function renderWithRouter(ui: React.ReactElement) {
|
|
|
|
|
return render(<BrowserRouter>{ui}</BrowserRouter>);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-13 13:24:03 -08:00
|
|
|
describe("StandingsTable", () => {
|
2025-11-14 20:01:21 -08:00
|
|
|
const mockLeagueId = "league-123";
|
|
|
|
|
const mockSeasonId = "season-456";
|
|
|
|
|
|
2025-11-13 13:24:03 -08:00
|
|
|
const mockStandings: TeamStanding[] = [
|
|
|
|
|
{
|
|
|
|
|
teamId: "team1",
|
|
|
|
|
teamName: "Champions United",
|
|
|
|
|
totalPoints: 250.5,
|
|
|
|
|
currentRank: 1,
|
|
|
|
|
previousRank: 2,
|
2026-03-22 11:05:13 -07:00
|
|
|
rankChange: 1,
|
2025-11-13 13:24:03 -08:00
|
|
|
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,
|
2026-03-22 11:05:13 -07:00
|
|
|
rankChange: -1,
|
2025-11-13 13:24:03 -08:00
|
|
|
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,
|
2026-03-22 11:05:13 -07:00
|
|
|
rankChange: 0,
|
2025-11-13 13:24:03 -08:00
|
|
|
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", () => {
|
2025-11-14 20:01:21 -08:00
|
|
|
renderWithRouter(<StandingsTable standings={mockStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
|
|
|
|
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", () => {
|
2025-11-14 20:01:21 -08:00
|
|
|
renderWithRouter(<StandingsTable standings={mockStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
Add rules page, rewrite how-to-play, and fix scoring display (#20)
- Add new /rules route with official league rules covering rosters,
scoring, major-based QP scoring, tiebreakers, draft, and season rules
- Rewrite how-to-play page with a tutorial/marketing tone, highlighting
the Fischer increment draft clock as a novel feature
- Add Rules link to navbar (desktop and mobile)
- Align QP values in both pages with DEFAULT_QP_VALUES in code
- Fix tiebreaker description to match placement-count logic in code
- Update all fantasy point displays from toFixed(1) to toFixed(2) across
StandingsTable, TeamScoreBreakdown, and PointProgressionChart
- Update tests to match new two-decimal-place point display format
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 21:58:51 -08:00
|
|
|
expect(screen.getByText("250.50")).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText("245.00")).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText("200.00")).toBeInTheDocument();
|
2025-11-13 13:24:03 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should show empty state when no standings", () => {
|
2025-11-14 20:01:21 -08:00
|
|
|
renderWithRouter(<StandingsTable standings={[]} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
|
|
|
|
expect(screen.getByText("No standings data available")).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("Rank Badges", () => {
|
|
|
|
|
it("should show trophy icon for 1st place", () => {
|
2025-11-14 20:01:21 -08:00
|
|
|
renderWithRouter(<StandingsTable standings={mockStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
|
|
|
|
expect(screen.getByText(/🏆.*1st/)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should show silver medal icon for 2nd place", () => {
|
2025-11-14 20:01:21 -08:00
|
|
|
renderWithRouter(<StandingsTable standings={mockStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
|
|
|
|
expect(screen.getByText(/🥈.*2nd/)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should show bronze medal icon for 3rd place", () => {
|
2025-11-14 20:01:21 -08:00
|
|
|
renderWithRouter(<StandingsTable standings={mockStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
|
|
|
|
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",
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
2025-11-14 20:01:21 -08:00
|
|
|
renderWithRouter(<StandingsTable standings={standings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
|
|
|
|
const badge = screen.getByText("4");
|
|
|
|
|
expect(badge).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("Movement Indicators", () => {
|
|
|
|
|
it("should show up arrow for positive rank change", () => {
|
2025-11-14 20:01:21 -08:00
|
|
|
renderWithRouter(<StandingsTable standings={mockStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
|
|
|
|
expect(screen.getByText("↑1")).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should show down arrow for negative rank change", () => {
|
2025-11-14 20:01:21 -08:00
|
|
|
renderWithRouter(<StandingsTable standings={mockStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
|
|
|
|
expect(screen.getByText("↓1")).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should not show indicator for no rank change", () => {
|
2025-11-14 20:01:21 -08:00
|
|
|
renderWithRouter(<StandingsTable standings={mockStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
2026-03-22 11:05:13 -07:00
|
|
|
// Only first two teams have rank changes; use \d to avoid matching sort-header arrows
|
|
|
|
|
const arrows = screen.queryAllByText(/[↑↓]\d/);
|
|
|
|
|
expect(arrows).toHaveLength(2);
|
2025-11-13 13:24:03 -08:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-22 11:05:13 -07:00
|
|
|
describe("7-Day Change Column", () => {
|
|
|
|
|
it("should show dash when sevenDayPointChange is not present", () => {
|
|
|
|
|
renderWithRouter(<StandingsTable standings={mockStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
|
|
|
|
|
|
|
|
|
// All mock standings lack sevenDayPointChange, so all rows show —
|
|
|
|
|
const dashes = screen.getAllByText("—");
|
|
|
|
|
expect(dashes.length).toBeGreaterThanOrEqual(3);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should display positive point change in green with + prefix", () => {
|
|
|
|
|
const standings: TeamStanding[] = [
|
|
|
|
|
{ ...mockStandings[0], sevenDayPointChange: 12.5 },
|
2025-11-13 13:24:03 -08:00
|
|
|
];
|
|
|
|
|
|
2026-03-22 11:05:13 -07:00
|
|
|
renderWithRouter(<StandingsTable standings={standings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
2026-03-22 11:05:13 -07:00
|
|
|
expect(screen.getByText("+12.50 pts")).toBeInTheDocument();
|
2025-11-13 13:24:03 -08:00
|
|
|
});
|
|
|
|
|
|
2026-03-22 11:05:13 -07:00
|
|
|
it("should display negative point change with no + prefix", () => {
|
|
|
|
|
const standings: TeamStanding[] = [
|
|
|
|
|
{ ...mockStandings[0], sevenDayPointChange: -3.25 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
renderWithRouter(<StandingsTable standings={standings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
2026-03-22 11:05:13 -07:00
|
|
|
expect(screen.getByText("-3.25 pts")).toBeInTheDocument();
|
2025-11-13 13:24:03 -08:00
|
|
|
});
|
|
|
|
|
|
2026-03-22 11:05:13 -07:00
|
|
|
it("should show rank change within the 7-day column", () => {
|
|
|
|
|
const standings: TeamStanding[] = [
|
|
|
|
|
{ ...mockStandings[0], sevenDayPointChange: 10, rankChange: 2 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
renderWithRouter(<StandingsTable standings={standings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
2026-03-22 11:05:13 -07:00
|
|
|
expect(screen.getByText("↑2 rank")).toBeInTheDocument();
|
2025-11-13 13:24:03 -08:00
|
|
|
});
|
|
|
|
|
|
2026-03-22 11:05:13 -07:00
|
|
|
it("should show no rank change indicator when rank is unchanged", () => {
|
2025-11-13 13:24:03 -08:00
|
|
|
const standings: TeamStanding[] = [
|
2026-03-22 11:05:13 -07:00
|
|
|
{ ...mockStandings[0], sevenDayPointChange: 5, rankChange: 0 },
|
2025-11-13 13:24:03 -08:00
|
|
|
];
|
|
|
|
|
|
2026-03-22 11:05:13 -07:00
|
|
|
renderWithRouter(<StandingsTable standings={standings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
2026-03-22 11:05:13 -07:00
|
|
|
expect(screen.getByText("— rank")).toBeInTheDocument();
|
2025-11-13 13:24:03 -08:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("Participants Remaining", () => {
|
|
|
|
|
it("should show remaining count when participants are incomplete", () => {
|
2025-11-14 20:01:21 -08:00
|
|
|
renderWithRouter(<StandingsTable standings={mockStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
|
|
|
|
expect(screen.getByText("3 remaining")).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText("5 remaining")).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should show 'Complete' badge when all participants done", () => {
|
2025-11-14 20:01:21 -08:00
|
|
|
renderWithRouter(<StandingsTable standings={mockStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
|
|
|
|
expect(screen.getByText("Complete")).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("Table Structure", () => {
|
|
|
|
|
it("should have correct table headers", () => {
|
2025-11-14 20:01:21 -08:00
|
|
|
renderWithRouter(<StandingsTable standings={mockStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
|
|
|
|
expect(screen.getByText("Rank")).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText("Team")).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText("Points")).toBeInTheDocument();
|
2026-03-22 11:05:13 -07:00
|
|
|
expect(screen.getByText("7-Day Change")).toBeInTheDocument();
|
2025-11-13 13:24:03 -08:00
|
|
|
expect(screen.getByText("Remaining")).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-22 11:05:13 -07:00
|
|
|
it("should render teams in order by rank", () => {
|
2025-11-14 20:01:21 -08:00
|
|
|
const { container } = renderWithRouter(<StandingsTable standings={mockStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
|
|
|
|
const rows = container.querySelectorAll("tbody tr");
|
|
|
|
|
expect(rows).toHaveLength(3);
|
|
|
|
|
|
|
|
|
|
expect(rows[0].textContent).toContain("Champions United");
|
|
|
|
|
expect(rows[1].textContent).toContain("Second Place Squad");
|
|
|
|
|
expect(rows[2].textContent).toContain("Bronze Warriors");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("Tied Ranks", () => {
|
|
|
|
|
it("should handle teams with same rank", () => {
|
|
|
|
|
const tiedStandings: TeamStanding[] = [
|
2026-03-22 11:05:13 -07:00
|
|
|
{ ...mockStandings[0], currentRank: 1, teamName: "Team A" },
|
|
|
|
|
{ ...mockStandings[1], currentRank: 1, teamName: "Team B" },
|
|
|
|
|
{ ...mockStandings[2], currentRank: 3, teamName: "Team C" },
|
2025-11-13 13:24:03 -08:00
|
|
|
];
|
|
|
|
|
|
2025-11-14 20:01:21 -08:00
|
|
|
renderWithRouter(<StandingsTable standings={tiedStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
|
|
|
|
const firstPlaceBadges = screen.getAllByText(/🏆.*1st/);
|
|
|
|
|
expect(firstPlaceBadges).toHaveLength(2);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText(/🥉.*3rd/)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("Accessibility", () => {
|
|
|
|
|
it("should have title attributes for movement indicators", () => {
|
2025-11-14 20:01:21 -08:00
|
|
|
const { container } = renderWithRouter(<StandingsTable standings={mockStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
2025-11-13 13:24:03 -08:00
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|