Fix standings page team icons to use selected team avatar (#388)
* Fix standings page team icons to use selected team avatar
The full standings page was not passing avatar data (logoUrl, flagConfig,
avatarType, ownerAvatarData) to StandingsPreview entries, so all teams
showed the default initials fallback instead of their chosen avatar.
Fetches the avatar columns from the teams table in the loader, resolves
owner avatar data for teams using the owner avatar type, and threads
all avatar fields through formattedStandings into previewEntries.
https://claude.ai/code/session_01CVt5Vo2PDGY4G3wSWbsmRG
* Add StandingsPreview component tests covering avatar rendering
Tests were missing for the StandingsPreview component, which is the
component that actually renders team avatars on the full standings page.
Covers uploaded image, flag SVG, owner avatar inheritance, and the
generated-flag fallback, plus basic content rendering (team names,
owner names, points, description, full standings link).
https://claude.ai/code/session_01CVt5Vo2PDGY4G3wSWbsmRG
* Fix StandingsPreview test ambiguous text query for team names
FlagSvg renders a <title> element with the team name for accessibility,
so getByText("Team Alpha") matched two elements. Scope the query to the
<p> element to target only the visible team name display.
https://claude.ai/code/session_01CVt5Vo2PDGY4G3wSWbsmRG
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-06 18:08:46 -07:00
|
|
|
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");
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-06-21 11:56:15 -07:00
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
});
|
Fix standings page team icons to use selected team avatar (#388)
* Fix standings page team icons to use selected team avatar
The full standings page was not passing avatar data (logoUrl, flagConfig,
avatarType, ownerAvatarData) to StandingsPreview entries, so all teams
showed the default initials fallback instead of their chosen avatar.
Fetches the avatar columns from the teams table in the loader, resolves
owner avatar data for teams using the owner avatar type, and threads
all avatar fields through formattedStandings into previewEntries.
https://claude.ai/code/session_01CVt5Vo2PDGY4G3wSWbsmRG
* Add StandingsPreview component tests covering avatar rendering
Tests were missing for the StandingsPreview component, which is the
component that actually renders team avatars on the full standings page.
Covers uploaded image, flag SVG, owner avatar inheritance, and the
generated-flag fallback, plus basic content rendering (team names,
owner names, points, description, full standings link).
https://claude.ai/code/session_01CVt5Vo2PDGY4G3wSWbsmRG
* Fix StandingsPreview test ambiguous text query for team names
FlagSvg renders a <title> element with the team name for accessibility,
so getByText("Team Alpha") matched two elements. Scope the query to the
<p> element to target only the visible team name display.
https://claude.ai/code/session_01CVt5Vo2PDGY4G3wSWbsmRG
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-06 18:08:46 -07:00
|
|
|
});
|