Adds live standings sync and display for bracket-based sports (NBA/NHL), so league members can see W/L tables and which teams their opponents drafted during the regular season — not just after the playoff bracket is set. - New `regular_season_standings` table with upsert-on-conflict sync - Standings sync service with NHL (api-web.nhle.com) and NBA (ESPN) adapters, externalId write-back for future syncs, and unmatched-team resolution UI - `RegularSeasonStandings` component: flat (NBA) + division/wild-card (NHL) modes, playoff line, TeamOwnerBadge, projected Brackt points (EV), mobile horizontal scroll - Admin "Sync Standings" card + "Resolve Unmatched" UI on sports season page - Admin manual standings edit hatch at /admin/sports-seasons/:id/regular-standings - Show standings above bracket until matches exist; below once bracket is set - `normalize-team-name` utility extracted to shared lib Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
141 lines
3.6 KiB
TypeScript
141 lines
3.6 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { NhlStandingsAdapter } from "../nhl";
|
|
|
|
const SAMPLE_NHL_RESPONSE = {
|
|
standings: [
|
|
{
|
|
teamName: { default: "Boston Bruins" },
|
|
teamAbbrev: { default: "BOS" },
|
|
conferenceName: "Eastern",
|
|
divisionName: "Atlantic",
|
|
gamesPlayed: 68,
|
|
wins: 42,
|
|
losses: 20,
|
|
otLosses: 6,
|
|
points: 90,
|
|
pointPctg: 0.662,
|
|
winPctg: 0.618,
|
|
l10Wins: 7,
|
|
l10Losses: 2,
|
|
l10OtLosses: 1,
|
|
streakCode: "W",
|
|
streakCount: 3,
|
|
homeWins: 24,
|
|
homeLosses: 9,
|
|
homeOtLosses: 2,
|
|
roadWins: 18,
|
|
roadLosses: 11,
|
|
roadOtLosses: 4,
|
|
conferenceSequence: 1,
|
|
divisionSequence: 1,
|
|
leagueSequence: 1,
|
|
},
|
|
{
|
|
teamName: { default: "Toronto Maple Leafs" },
|
|
teamAbbrev: { default: "TOR" },
|
|
conferenceName: "Eastern",
|
|
divisionName: "Atlantic",
|
|
gamesPlayed: 70,
|
|
wins: 38,
|
|
losses: 25,
|
|
otLosses: 7,
|
|
points: 83,
|
|
pointPctg: 0.593,
|
|
winPctg: 0.543,
|
|
l10Wins: 5,
|
|
l10Losses: 4,
|
|
l10OtLosses: 1,
|
|
streakCode: "L",
|
|
streakCount: 2,
|
|
homeWins: 20,
|
|
homeLosses: 12,
|
|
homeOtLosses: 3,
|
|
roadWins: 18,
|
|
roadLosses: 13,
|
|
roadOtLosses: 4,
|
|
conferenceSequence: 3,
|
|
divisionSequence: 2,
|
|
leagueSequence: 5,
|
|
},
|
|
],
|
|
};
|
|
|
|
describe("NhlStandingsAdapter", () => {
|
|
beforeEach(() => {
|
|
vi.stubGlobal("fetch", vi.fn());
|
|
});
|
|
|
|
it("maps NHL API response to FetchedStandingsRecord[]", async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => SAMPLE_NHL_RESPONSE,
|
|
} as Response);
|
|
|
|
const adapter = new NhlStandingsAdapter();
|
|
const records = await adapter.fetchStandings();
|
|
|
|
expect(records).toHaveLength(2);
|
|
|
|
const bos = records[0];
|
|
expect(bos.teamName).toBe("Boston Bruins");
|
|
expect(bos.externalTeamId).toBe("BOS");
|
|
expect(bos.wins).toBe(42);
|
|
expect(bos.losses).toBe(20);
|
|
expect(bos.otLosses).toBe(6);
|
|
expect(bos.gamesPlayed).toBe(68);
|
|
expect(bos.conference).toBe("Eastern");
|
|
expect(bos.division).toBe("Atlantic");
|
|
expect(bos.conferenceRank).toBe(1);
|
|
expect(bos.divisionRank).toBe(1);
|
|
expect(bos.leagueRank).toBe(1);
|
|
});
|
|
|
|
it("formats streak correctly", async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => SAMPLE_NHL_RESPONSE,
|
|
} as Response);
|
|
|
|
const adapter = new NhlStandingsAdapter();
|
|
const records = await adapter.fetchStandings();
|
|
|
|
expect(records[0].streak).toBe("W3");
|
|
expect(records[1].streak).toBe("L2");
|
|
});
|
|
|
|
it("formats lastTen as W-L-OTL", async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => SAMPLE_NHL_RESPONSE,
|
|
} as Response);
|
|
|
|
const adapter = new NhlStandingsAdapter();
|
|
const records = await adapter.fetchStandings();
|
|
|
|
expect(records[0].lastTen).toBe("7-2-1");
|
|
});
|
|
|
|
it("formats home and away records", async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => SAMPLE_NHL_RESPONSE,
|
|
} as Response);
|
|
|
|
const adapter = new NhlStandingsAdapter();
|
|
const records = await adapter.fetchStandings();
|
|
|
|
expect(records[0].homeRecord).toBe("24-9-2");
|
|
expect(records[0].awayRecord).toBe("18-11-4");
|
|
});
|
|
|
|
it("throws on non-ok response", async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 503,
|
|
statusText: "Service Unavailable",
|
|
} as Response);
|
|
|
|
const adapter = new NhlStandingsAdapter();
|
|
await expect(adapter.fetchStandings()).rejects.toThrow("503");
|
|
});
|
|
});
|