All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m23s
🚀 Deploy / 🧪 Test (push) Successful in 3m9s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m20s
🚀 Deploy / 🐳 Build (push) Successful in 1m9s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m23s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
Surface projected final points on the full standings page and restructure the rank/point change indicators so columns align cleanly on desktop and mobile. - StatHelpers: stacked label/value/delta columns with a per-row reservable delta line; merge rank/point indicators into a single DeltaBadge; parameterize StatDivider height. - StandingsPreview: opt-in `showProjected` column (projected points only), gated on participants remaining; reserve the delta line only when a row actually moved (no stray em-dashes). - Full standings page: pass projected data + showProjected (hidden when the season is complete). - League home: fetch via getSevenDayStandingsChange so the preview shows the same 7-day rank/point changes as the full standings page. - LeagueRow: top-align stats and restore h-8 dividers so the shared-component changes don't alter the league list rows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
156 lines
5.6 KiB
TypeScript
156 lines
5.6 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { render, screen } from "@testing-library/react";
|
|
import { BrowserRouter } from "react-router";
|
|
import { StandingsPreview, type StandingsPreviewEntry } from "../StandingsPreview";
|
|
|
|
function renderWithRouter(ui: React.ReactElement) {
|
|
return render(<BrowserRouter>{ui}</BrowserRouter>);
|
|
}
|
|
|
|
const baseEntry: StandingsPreviewEntry = {
|
|
teamId: "team-1",
|
|
teamName: "Lightning Wolves",
|
|
ownerName: "alice",
|
|
displayRank: 1,
|
|
currentRank: 1,
|
|
points: 2500,
|
|
href: "/leagues/1/standings/1/teams/team-1",
|
|
};
|
|
|
|
describe("StandingsPreview", () => {
|
|
describe("Avatar rendering", () => {
|
|
it("renders an img for uploaded avatar type", () => {
|
|
const entry: StandingsPreviewEntry = {
|
|
...baseEntry,
|
|
avatarType: "uploaded",
|
|
logoUrl: "https://example.com/team-logo.png",
|
|
};
|
|
renderWithRouter(<StandingsPreview entries={[entry]} />);
|
|
|
|
const img = screen.getByAltText("Lightning Wolves");
|
|
expect(img.tagName).toBe("IMG");
|
|
});
|
|
|
|
it("renders an SVG for flag avatar type", () => {
|
|
const entry: StandingsPreviewEntry = {
|
|
...baseEntry,
|
|
avatarType: "flag",
|
|
flagConfig: { pattern: "triband-h", colors: ["#ff0000", "#ffffff", "#0000ff"] },
|
|
};
|
|
const { container } = renderWithRouter(<StandingsPreview entries={[entry]} />);
|
|
|
|
expect(container.querySelector("svg")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders owner image when avatarType is owner with image ownerAvatarData", () => {
|
|
const entry: StandingsPreviewEntry = {
|
|
...baseEntry,
|
|
avatarType: "owner",
|
|
ownerAvatarData: { type: "image", url: "https://example.com/owner.png" },
|
|
};
|
|
renderWithRouter(<StandingsPreview entries={[entry]} />);
|
|
|
|
const img = screen.getByAltText("Lightning Wolves");
|
|
expect(img.tagName).toBe("IMG");
|
|
});
|
|
|
|
it("falls back to a generated flag when no avatar data is provided", () => {
|
|
const entry: StandingsPreviewEntry = { ...baseEntry, avatarType: undefined };
|
|
const { container } = renderWithRouter(<StandingsPreview entries={[entry]} />);
|
|
|
|
expect(container.querySelector("svg")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("Basic rendering", () => {
|
|
it("renders team names for all entries", () => {
|
|
const entries: StandingsPreviewEntry[] = [
|
|
{ ...baseEntry, teamId: "t1", teamName: "Team Alpha", displayRank: 1 },
|
|
{ ...baseEntry, teamId: "t2", teamName: "Team Beta", displayRank: 2 },
|
|
];
|
|
renderWithRouter(<StandingsPreview entries={entries} />);
|
|
|
|
// Use selector to target only the <p> element — the team name also appears
|
|
// in the SVG <title> for accessibility, causing multiple matches otherwise.
|
|
expect(screen.getByText("Team Alpha", { selector: "p" })).toBeInTheDocument();
|
|
expect(screen.getByText("Team Beta", { selector: "p" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders owner name below team name", () => {
|
|
renderWithRouter(<StandingsPreview entries={[baseEntry]} />);
|
|
|
|
expect(screen.getByText("alice")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders rounded points", () => {
|
|
renderWithRouter(<StandingsPreview entries={[{ ...baseEntry, points: 1500 }]} />);
|
|
|
|
expect(screen.getByText("1,500")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders description when provided", () => {
|
|
renderWithRouter(<StandingsPreview entries={[baseEntry]} description="Current Standings" />);
|
|
|
|
expect(screen.getByText("Current Standings")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders full standings link when fullStandingsHref is provided", () => {
|
|
renderWithRouter(
|
|
<StandingsPreview entries={[baseEntry]} fullStandingsHref="/leagues/1/standings/1" />
|
|
);
|
|
|
|
const link = screen.getByRole("link", { name: "Full Standings" });
|
|
expect(link).toHaveAttribute("href", "/leagues/1/standings/1");
|
|
});
|
|
});
|
|
|
|
describe("Projections", () => {
|
|
it("does not render a Projected column by default", () => {
|
|
const entry: StandingsPreviewEntry = {
|
|
...baseEntry,
|
|
projectedPoints: 3000,
|
|
participantsRemaining: 3,
|
|
};
|
|
renderWithRouter(<StandingsPreview entries={[entry]} />);
|
|
|
|
expect(screen.queryByText("Projected")).not.toBeInTheDocument();
|
|
expect(screen.queryByText("3,000")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("renders the projected value when showProjected and participants remain", () => {
|
|
const entry: StandingsPreviewEntry = {
|
|
...baseEntry,
|
|
projectedPoints: 3000,
|
|
participantsRemaining: 3,
|
|
};
|
|
renderWithRouter(<StandingsPreview entries={[entry]} showProjected />);
|
|
|
|
expect(screen.getByText("Projected")).toBeInTheDocument();
|
|
expect(screen.getByText("3,000")).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows a placeholder instead of a value when no participants remain", () => {
|
|
const entry: StandingsPreviewEntry = {
|
|
...baseEntry,
|
|
projectedPoints: 9999,
|
|
participantsRemaining: 0,
|
|
};
|
|
renderWithRouter(<StandingsPreview entries={[entry]} showProjected />);
|
|
|
|
// Column header still present, but the projected total is not shown.
|
|
expect(screen.getByText("Projected")).toBeInTheDocument();
|
|
expect(screen.queryByText("9,999")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("shows a placeholder when projectedPoints is missing", () => {
|
|
const entry: StandingsPreviewEntry = {
|
|
...baseEntry,
|
|
projectedPoints: null,
|
|
participantsRemaining: 3,
|
|
};
|
|
renderWithRouter(<StandingsPreview entries={[entry]} showProjected />);
|
|
|
|
expect(screen.getByText("Projected")).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|