import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { MlsStandingsAdapter } from "../mls"; function makeStat(name: string, value: number, displayValue?: string) { return { name, value, displayValue: displayValue ?? String(value) }; } const SAMPLE_MLS_RESPONSE = { children: [ { name: "Eastern Conference", standings: { entries: [ { team: { id: "371", displayName: "Inter Miami CF", shortDisplayName: "Inter Miami", abbreviation: "MIA" }, stats: [ makeStat("wins", 18), makeStat("losses", 8), makeStat("ties", 8), makeStat("points", 62), makeStat("pointsFor", 55), makeStat("pointsAgainst", 38), makeStat("pointDifferential", 17), makeStat("playoffSeed", 1), { name: "Home", value: 0, displayValue: "10-3-3" }, { name: "Road", value: 0, displayValue: "8-5-5" }, ], }, { team: { id: "928", displayName: "Columbus Crew", shortDisplayName: "Columbus", abbreviation: "CLB" }, stats: [ makeStat("wins", 15), makeStat("losses", 10), makeStat("ties", 9), makeStat("points", 54), makeStat("pointsFor", 48), makeStat("pointsAgainst", 42), makeStat("pointDifferential", 6), makeStat("playoffSeed", 2), { name: "Home", value: 0, displayValue: "9-4-4" }, { name: "Road", value: 0, displayValue: "6-6-5" }, ], }, ], }, }, { name: "Western Conference", standings: { entries: [ { team: { id: "339", displayName: "Los Angeles FC", shortDisplayName: "LAFC", abbreviation: "LAFC" }, stats: [ makeStat("wins", 20), makeStat("losses", 6), makeStat("ties", 8), makeStat("points", 68), makeStat("pointsFor", 62), makeStat("pointsAgainst", 30), makeStat("pointDifferential", 32), makeStat("playoffSeed", 1), { name: "Home", value: 0, displayValue: "12-2-3" }, { name: "Road", value: 0, displayValue: "8-4-5" }, ], }, ], }, }, ], }; describe("MlsStandingsAdapter", () => { beforeEach(() => { vi.stubGlobal("fetch", vi.fn()); }); afterEach(() => { vi.restoreAllMocks(); }); it("maps ESPN API response to FetchedStandingsRecord[]", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLS_RESPONSE, } as Response); const adapter = new MlsStandingsAdapter(); const records = await adapter.fetchStandings(); expect(records).toHaveLength(3); }); it("maps soccer-specific fields (W/D/L/GF/GA/GD/PTS)", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLS_RESPONSE, } as Response); const adapter = new MlsStandingsAdapter(); const records = await adapter.fetchStandings(); const miami = records.find((r) => r.teamName === "Inter Miami CF"); if (!miami) throw new Error("Inter Miami CF not found"); expect(miami.wins).toBe(18); expect(miami.losses).toBe(8); expect(miami.ties).toBe(8); expect(miami.tablePoints).toBe(62); expect(miami.goalsFor).toBe(55); expect(miami.goalsAgainst).toBe(38); expect(miami.goalDifference).toBe(17); expect(miami.gamesPlayed).toBe(34); // 18+8+8 }); it("normalizes conference names to Eastern/Western", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLS_RESPONSE, } as Response); const adapter = new MlsStandingsAdapter(); const records = await adapter.fetchStandings(); const miami = records.find((r) => r.teamName === "Inter Miami CF"); const lafc = records.find((r) => r.teamName === "Los Angeles FC"); if (!miami || !lafc) throw new Error("Teams not found"); expect(miami.conference).toBe("Eastern"); expect(lafc.conference).toBe("Western"); }); it("assigns conferenceRank from playoffSeed stat", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLS_RESPONSE, } as Response); const adapter = new MlsStandingsAdapter(); const records = await adapter.fetchStandings(); const miami = records.find((r) => r.teamName === "Inter Miami CF"); const columbus = records.find((r) => r.teamName === "Columbus Crew"); if (!miami || !columbus) throw new Error("Teams not found"); expect(miami.conferenceRank).toBe(1); expect(columbus.conferenceRank).toBe(2); }); it("assigns leagueRank by points descending", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLS_RESPONSE, } as Response); const adapter = new MlsStandingsAdapter(); const records = await adapter.fetchStandings(); // LAFC has 68 pts, Miami 62, Columbus 54 → ranks 1, 2, 3 const lafc = records.find((r) => r.teamName === "Los Angeles FC"); const miami = records.find((r) => r.teamName === "Inter Miami CF"); const columbus = records.find((r) => r.teamName === "Columbus Crew"); if (!lafc || !miami || !columbus) throw new Error("Teams not found"); expect(lafc.leagueRank).toBe(1); expect(miami.leagueRank).toBe(2); expect(columbus.leagueRank).toBe(3); }); it("uses externalTeamId from ESPN team id", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLS_RESPONSE, } as Response); const adapter = new MlsStandingsAdapter(); const records = await adapter.fetchStandings(); const lafc = records.find((r) => r.teamName === "Los Angeles FC"); if (!lafc) throw new Error("LAFC not found"); expect(lafc.externalTeamId).toBe("339"); }); it("maps home and away records", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLS_RESPONSE, } as Response); const adapter = new MlsStandingsAdapter(); const records = await adapter.fetchStandings(); const miami = records.find((r) => r.teamName === "Inter Miami CF"); if (!miami) throw new Error("Inter Miami CF not found"); expect(miami.homeRecord).toBe("10-3-3"); expect(miami.awayRecord).toBe("8-5-5"); }); it("throws on non-ok response", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: false, status: 429, statusText: "Too Many Requests", } as Response); const adapter = new MlsStandingsAdapter(); await expect(adapter.fetchStandings()).rejects.toThrow("429"); }); it("throws when no entries returned", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ children: [] }), } as Response); const adapter = new MlsStandingsAdapter(); await expect(adapter.fetchStandings()).rejects.toThrow("no entries"); }); it("passes through unrecognized conference names unchanged", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ children: [ { name: "Southern Division", standings: { entries: [ { team: { id: "1", displayName: "Test FC" }, stats: [ makeStat("wins", 5), makeStat("losses", 3), makeStat("ties", 2), makeStat("points", 17), makeStat("pointsFor", 12), makeStat("pointsAgainst", 9), makeStat("pointDifferential", 3), makeStat("playoffSeed", 1), ], }, ], }, }, ], }), } as Response); const adapter = new MlsStandingsAdapter(); const records = await adapter.fetchStandings(); expect(records[0].conference).toBe("Southern Division"); }); it("returns winPct of 0 when gamesPlayed is 0", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ children: [ { name: "Eastern Conference", standings: { entries: [ { team: { id: "1", displayName: "Preseason FC" }, stats: [ makeStat("wins", 0), makeStat("losses", 0), makeStat("ties", 0), makeStat("points", 0), makeStat("pointsFor", 0), makeStat("pointsAgainst", 0), makeStat("pointDifferential", 0), makeStat("playoffSeed", 1), ], }, ], }, }, ], }), } as Response); const adapter = new MlsStandingsAdapter(); const records = await adapter.fetchStandings(); expect(records[0].winPct).toBe(0); expect(records[0].gamesPlayed).toBe(0); }); });