diff --git a/app/components/league/__tests__/StandingsPreview.test.tsx b/app/components/league/__tests__/StandingsPreview.test.tsx new file mode 100644 index 0000000..14b7636 --- /dev/null +++ b/app/components/league/__tests__/StandingsPreview.test.tsx @@ -0,0 +1,106 @@ +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({ui}); +} + +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(); + + 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(); + + 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(); + + 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(); + + 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(); + + // Use selector to target only the

element — the team name also appears + // in the SVG 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"); + }); + }); +}); diff --git a/app/routes/leagues/$leagueId.standings.$seasonId.tsx b/app/routes/leagues/$leagueId.standings.$seasonId.tsx index c21bede..00e345a 100644 --- a/app/routes/leagues/$leagueId.standings.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.standings.$seasonId.tsx @@ -10,6 +10,7 @@ import { isSeasonComplete, getSeasonCompletionPercentage } from "~/lib/season-he import { getRecentTeamScoreEvents } from "~/models/team-score-events"; import { PointProgressionChart } from "~/components/standings/PointProgressionChart"; import { findUsersByIds, getUserDisplayName } from "~/models/user"; +import { resolveUserAvatarData } from "~/lib/avatar-data"; import { StandingsPreview } from "~/components/league/StandingsPreview"; import { buildTiedRankChecker, getDisplayRank } from "~/lib/standings-display"; import { RecentScoresCard } from "~/components/standings/RecentScoresCard"; @@ -59,7 +60,7 @@ export async function loader(args: Route.LoaderArgs) { getSevenDayStandingsChange(seasonId, db), db.query.teams.findMany({ where: eq(schema.teams.seasonId, seasonId), - columns: { id: true, ownerId: true }, + columns: { id: true, ownerId: true, logoUrl: true, flagConfig: true, avatarType: true }, }), getSeasonPointProgression(seasonId, db), isSeasonComplete(seasonId, db), @@ -80,12 +81,23 @@ export async function loader(args: Route.LoaderArgs) { }) ); + const ownerAvatarDataByUserId = new Map(userRows.map((u) => [u.id, resolveUserAvatarData(u)])); + const teamById = new Map(teams.map((t) => [t.id, t])); + // Map to format expected by component (use sevenDayRankChange instead of previousRank) - const formattedStandings = standingsWithComparison.map((standing) => ({ - ...standing, - rankChange: standing.sevenDayRankChange, - ownerName: ownerNameByTeamId.get(standing.teamId) ?? null, - })); + const formattedStandings = standingsWithComparison.map((standing) => { + const team = teamById.get(standing.teamId); + const ownerAvatarData = team?.ownerId ? (ownerAvatarDataByUserId.get(team.ownerId) ?? null) : null; + return { + ...standing, + rankChange: standing.sevenDayRankChange, + ownerName: ownerNameByTeamId.get(standing.teamId) ?? null, + logoUrl: team?.logoUrl ?? null, + flagConfig: team?.flagConfig ?? null, + avatarType: team?.avatarType ?? null, + ownerAvatarData, + }; + }); // Enrich recentScoreEvents with owner names const enrichedScoreEvents = recentScoreEvents.map((event) => ({ @@ -119,6 +131,10 @@ export default function LeagueStandings() { teamId: s.teamId, teamName: s.teamName, ownerName: s.ownerName, + logoUrl: s.logoUrl, + flagConfig: s.flagConfig, + avatarType: s.avatarType, + ownerAvatarData: s.ownerAvatarData, displayRank: getDisplayRank(s, standings.length, isTied(s.currentRank)), currentRank: s.currentRank, points: s.totalPoints,