import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { WnbaStandingsAdapter } from "../wnba"; function makeStat(name: string, value: number, displayValue?: string) { return { name, value, displayValue: displayValue ?? String(value) }; } // WNBA standings response (13 active teams — flat conference structure, no divisions). const SAMPLE_STANDINGS_RESPONSE = { children: [ { name: "Eastern Conference", standings: { entries: [ { team: { id: "5", displayName: "New York Liberty", abbreviation: "NY" }, stats: [ makeStat("wins", 28), makeStat("losses", 12), makeStat("winPercent", 0.7), makeStat("gamesBehind", 0), makeStat("playoffSeed", 1), { name: "streak", value: 3, displayValue: "W3" }, { name: "Last Ten Games", value: 0, displayValue: "8-2" }, { name: "Home", value: 0, displayValue: "15-5" }, { name: "Road", value: 0, displayValue: "13-7" }, makeStat("avgPointsFor", 89.5), makeStat("avgPointsAgainst", 82.1), ], }, { team: { id: "3", displayName: "Connecticut Sun", abbreviation: "CONN" }, stats: [ makeStat("wins", 20), makeStat("losses", 20), makeStat("winPercent", 0.5), makeStat("gamesBehind", 8), makeStat("playoffSeed", 4), { name: "streak", value: 1, displayValue: "L1" }, { name: "Last Ten Games", value: 0, displayValue: "5-5" }, { name: "Home", value: 0, displayValue: "11-9" }, { name: "Road", value: 0, displayValue: "9-11" }, makeStat("avgPointsFor", 78.0), makeStat("avgPointsAgainst", 78.0), ], }, ], }, }, { name: "Western Conference", standings: { entries: [ { team: { id: "20", displayName: "Las Vegas Aces", abbreviation: "LV" }, stats: [ makeStat("wins", 26), makeStat("losses", 14), makeStat("winPercent", 0.65), makeStat("gamesBehind", 2), makeStat("playoffSeed", 1), { name: "streak", value: 2, displayValue: "W2" }, { name: "Last Ten Games", value: 0, displayValue: "7-3" }, { name: "Home", value: 0, displayValue: "14-6" }, { name: "Road", value: 0, displayValue: "12-8" }, makeStat("avgPointsFor", 91.2), makeStat("avgPointsAgainst", 85.0), ], }, ], }, }, ], }; // Teams endpoint response — includes 2 expansion teams not yet in standings. const SAMPLE_TEAMS_RESPONSE = { sports: [ { leagues: [ { teams: [ { team: { id: "5", displayName: "New York Liberty", isActive: true } }, { team: { id: "3", displayName: "Connecticut Sun", isActive: true } }, { team: { id: "20", displayName: "Las Vegas Aces", isActive: true } }, { team: { id: "99001", displayName: "Portland Fire", isActive: true } }, { team: { id: "99002", displayName: "Toronto Tempo", isActive: true } }, ], }, ], }, ], }; // Teams response with no expansion teams beyond what's in standings. const TEAMS_RESPONSE_NO_EXTRAS = { sports: [{ leagues: [{ teams: [ { team: { id: "5", displayName: "New York Liberty", isActive: true } }, { team: { id: "3", displayName: "Connecticut Sun", isActive: true } }, { team: { id: "20", displayName: "Las Vegas Aces", isActive: true } }, ] }] }], }; function mockFetch(standingsBody: unknown, teamsBody: unknown) { vi.mocked(fetch) .mockResolvedValueOnce({ ok: true, json: async () => standingsBody } as Response) .mockResolvedValueOnce({ ok: true, json: async () => teamsBody } as Response); } describe("WnbaStandingsAdapter", () => { beforeEach(() => { vi.stubGlobal("fetch", vi.fn()); }); afterEach(() => { vi.restoreAllMocks(); }); it("returns standings teams + zero-record expansion teams", async () => { mockFetch(SAMPLE_STANDINGS_RESPONSE, SAMPLE_TEAMS_RESPONSE); const adapter = new WnbaStandingsAdapter(); const records = await adapter.fetchStandings(); // 3 from standings + 2 expansion expect(records).toHaveLength(5); }); it("expansion teams have wins=0, losses=0, gamesPlayed=0, srs=null", async () => { mockFetch(SAMPLE_STANDINGS_RESPONSE, SAMPLE_TEAMS_RESPONSE); const adapter = new WnbaStandingsAdapter(); const records = await adapter.fetchStandings(); const portland = records.find((r) => r.teamName === "Portland Fire"); if (!portland) throw new Error("Portland Fire not found"); expect(portland.wins).toBe(0); expect(portland.losses).toBe(0); expect(portland.gamesPlayed).toBe(0); expect(portland.srs).toBeNull(); const toronto = records.find((r) => r.teamName === "Toronto Tempo"); if (!toronto) throw new Error("Toronto Tempo not found"); expect(toronto.wins).toBe(0); }); it("expansion teams rank below all active teams", async () => { mockFetch(SAMPLE_STANDINGS_RESPONSE, SAMPLE_TEAMS_RESPONSE); const adapter = new WnbaStandingsAdapter(); const records = await adapter.fetchStandings(); const maxActiveRank = Math.max( ...records .filter((r) => r.wins > 0 || r.gamesPlayed > 0) .map((r) => r.leagueRank) ); const portland = records.find((r) => r.teamName === "Portland Fire"); const toronto = records.find((r) => r.teamName === "Toronto Tempo"); if (!portland) throw new Error("Portland Fire not found"); if (!toronto) throw new Error("Toronto Tempo not found"); expect(portland.leagueRank).toBeGreaterThan(maxActiveRank); expect(toronto.leagueRank).toBeGreaterThan(maxActiveRank); }); it("parses wins, losses, and gamesPlayed correctly", async () => { mockFetch(SAMPLE_STANDINGS_RESPONSE, SAMPLE_TEAMS_RESPONSE); const adapter = new WnbaStandingsAdapter(); const records = await adapter.fetchStandings(); const ny = records.find((r) => r.teamName === "New York Liberty"); if (!ny) throw new Error("New York Liberty not found"); expect(ny.wins).toBe(28); expect(ny.losses).toBe(12); expect(ny.gamesPlayed).toBe(40); }); it("assigns leagueRank 1 to the team with most wins", async () => { mockFetch(SAMPLE_STANDINGS_RESPONSE, SAMPLE_TEAMS_RESPONSE); const adapter = new WnbaStandingsAdapter(); const records = await adapter.fetchStandings(); const rank1 = records.find((r) => r.leagueRank === 1); expect(rank1?.teamName).toBe("New York Liberty"); }); it("assigns conference from the standings response", async () => { mockFetch(SAMPLE_STANDINGS_RESPONSE, SAMPLE_TEAMS_RESPONSE); const adapter = new WnbaStandingsAdapter(); const records = await adapter.fetchStandings(); const ny = records.find((r) => r.teamName === "New York Liberty"); expect(ny?.conference).toBe("Eastern Conference"); const lv = records.find((r) => r.teamName === "Las Vegas Aces"); expect(lv?.conference).toBe("Western Conference"); }); it("expansion teams have no division (WNBA has no divisions)", async () => { mockFetch(SAMPLE_STANDINGS_RESPONSE, SAMPLE_TEAMS_RESPONSE); const adapter = new WnbaStandingsAdapter(); const records = await adapter.fetchStandings(); for (const record of records) { expect(record.division).toBeUndefined(); } }); it("computes srs as net rating (avgPointsFor - avgPointsAgainst)", async () => { mockFetch(SAMPLE_STANDINGS_RESPONSE, SAMPLE_TEAMS_RESPONSE); const adapter = new WnbaStandingsAdapter(); const records = await adapter.fetchStandings(); const ny = records.find((r) => r.teamName === "New York Liberty"); expect(ny?.srs).toBeCloseTo(7.4, 1); // 89.5 - 82.1 const conn = records.find((r) => r.teamName === "Connecticut Sun"); expect(conn?.srs).toBeCloseTo(0, 1); // 78.0 - 78.0 }); it("does not duplicate teams that appear in both standings and teams list", async () => { mockFetch(SAMPLE_STANDINGS_RESPONSE, TEAMS_RESPONSE_NO_EXTRAS); const adapter = new WnbaStandingsAdapter(); const records = await adapter.fetchStandings(); expect(records).toHaveLength(3); const names = records.map((r) => r.teamName); expect(new Set(names).size).toBe(3); }); it("extracts streak and lastTen from standings", async () => { mockFetch(SAMPLE_STANDINGS_RESPONSE, SAMPLE_TEAMS_RESPONSE); const adapter = new WnbaStandingsAdapter(); const records = await adapter.fetchStandings(); const ny = records.find((r) => r.teamName === "New York Liberty"); expect(ny?.streak).toBe("W3"); expect(ny?.lastTen).toBe("8-2"); }); it("extracts home and away records", async () => { mockFetch(SAMPLE_STANDINGS_RESPONSE, SAMPLE_TEAMS_RESPONSE); const adapter = new WnbaStandingsAdapter(); const records = await adapter.fetchStandings(); const ny = records.find((r) => r.teamName === "New York Liberty"); expect(ny?.homeRecord).toBe("15-5"); expect(ny?.awayRecord).toBe("13-7"); }); it("throws on non-ok standings response", async () => { vi.mocked(fetch) .mockResolvedValueOnce({ ok: false, status: 503, statusText: "Service Unavailable" } as Response) .mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_TEAMS_RESPONSE } as Response); const adapter = new WnbaStandingsAdapter(); await expect(adapter.fetchStandings()).rejects.toThrow("503"); }); it("throws on non-ok teams response", async () => { vi.mocked(fetch) .mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_STANDINGS_RESPONSE } as Response) .mockResolvedValueOnce({ ok: false, status: 429, statusText: "Too Many Requests" } as Response); const adapter = new WnbaStandingsAdapter(); await expect(adapter.fetchStandings()).rejects.toThrow("429"); }); });