import { describe, it, expect, vi, beforeEach } from "vitest"; import { MlbStandingsAdapter } from "../mlb"; const SAMPLE_MLB_RESPONSE = { records: [ { // AL East division record teamRecords: [ { team: { id: 110, name: "Baltimore Orioles", league: { name: "American League" }, division: { name: "AL East" }, }, wins: 47, losses: 34, gamesPlayed: 81, winningPercentage: ".580", divisionRank: "1", leagueRank: "1", gamesBack: "-", streak: { streakCode: "W3", streakNumber: 3 }, records: { splitRecords: [ { type: "home", wins: 24, losses: 15 }, { type: "away", wins: 23, losses: 19 }, { type: "lastTen", wins: 7, losses: 3 }, ], }, }, { team: { id: 111, name: "Boston Red Sox", league: { name: "American League" }, division: { name: "AL East" }, }, wins: 42, losses: 39, gamesPlayed: 81, winningPercentage: ".519", divisionRank: "2", leagueRank: "4", gamesBack: "5.0", streak: { streakCode: "L2", streakNumber: 2 }, records: { splitRecords: [ { type: "home", wins: 22, losses: 18 }, { type: "away", wins: 20, losses: 21 }, { type: "lastTen", wins: 4, losses: 6 }, ], }, }, ], }, { // NL East division record teamRecords: [ { team: { id: 121, name: "New York Mets", league: { name: "National League" }, division: { name: "NL East" }, }, wins: 43, losses: 38, gamesPlayed: 81, winningPercentage: ".531", divisionRank: "1", leagueRank: "1", gamesBack: "-", streak: { streakCode: "W1", streakNumber: 1 }, records: { splitRecords: [ { type: "home", wins: 22, losses: 17 }, { type: "away", wins: 21, losses: 21 }, { type: "lastTen", wins: 6, losses: 4 }, ], }, }, ], }, ], }; describe("MlbStandingsAdapter", () => { beforeEach(() => { vi.stubGlobal("fetch", vi.fn()); }); it("maps AL teams to conference 'AL'", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLB_RESPONSE, } as Response); const adapter = new MlbStandingsAdapter(); const records = await adapter.fetchStandings(); const bos = records.find((r) => r.teamName === "Baltimore Orioles"); expect(bos?.conference).toBe("AL"); }); it("maps NL teams to conference 'NL'", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLB_RESPONSE, } as Response); const adapter = new MlbStandingsAdapter(); const records = await adapter.fetchStandings(); const mets = records.find((r) => r.teamName === "New York Mets"); expect(mets?.conference).toBe("NL"); }); it("maps division names correctly", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLB_RESPONSE, } as Response); const adapter = new MlbStandingsAdapter(); const records = await adapter.fetchStandings(); const bos = records.find((r) => r.teamName === "Baltimore Orioles"); const mets = records.find((r) => r.teamName === "New York Mets"); expect(bos?.division).toBe("AL East"); expect(mets?.division).toBe("NL East"); }); it("maps wins, losses, gamesPlayed, and winPct", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLB_RESPONSE, } as Response); const adapter = new MlbStandingsAdapter(); const records = await adapter.fetchStandings(); const ori = records.find((r) => r.teamName === "Baltimore Orioles"); expect(ori?.wins).toBe(47); expect(ori?.losses).toBe(34); expect(ori?.gamesPlayed).toBe(81); expect(ori?.winPct).toBeCloseTo(0.58, 2); }); it("formats streak as 'W3' or 'L2'", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLB_RESPONSE, } as Response); const adapter = new MlbStandingsAdapter(); const records = await adapter.fetchStandings(); const ori = records.find((r) => r.teamName === "Baltimore Orioles"); const bos = records.find((r) => r.teamName === "Boston Red Sox"); expect(ori?.streak).toBe("W3"); expect(bos?.streak).toBe("L2"); }); it("extracts lastTen from splitRecords", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLB_RESPONSE, } as Response); const adapter = new MlbStandingsAdapter(); const records = await adapter.fetchStandings(); const ori = records.find((r) => r.teamName === "Baltimore Orioles"); expect(ori?.lastTen).toBe("7-3"); }); it("extracts homeRecord and awayRecord from splitRecords", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLB_RESPONSE, } as Response); const adapter = new MlbStandingsAdapter(); const records = await adapter.fetchStandings(); const ori = records.find((r) => r.teamName === "Baltimore Orioles"); expect(ori?.homeRecord).toBe("24-15"); expect(ori?.awayRecord).toBe("23-19"); }); it("parses gamesBack as null for the division leader ('-')", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLB_RESPONSE, } as Response); const adapter = new MlbStandingsAdapter(); const records = await adapter.fetchStandings(); const ori = records.find((r) => r.teamName === "Baltimore Orioles"); expect(ori?.gamesBack).toBeUndefined(); }); it("parses gamesBack as a number for non-leaders", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLB_RESPONSE, } as Response); const adapter = new MlbStandingsAdapter(); const records = await adapter.fetchStandings(); const bos = records.find((r) => r.teamName === "Boston Red Sox"); expect(bos?.gamesBack).toBeCloseTo(5.0, 1); }); it("computes leagueRank sorted by wins desc across all 30 teams", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLB_RESPONSE, } as Response); const adapter = new MlbStandingsAdapter(); const records = await adapter.fetchStandings(); // Baltimore (47W) should rank #1 overall, Boston (42W) should rank #2 const ori = records.find((r) => r.teamName === "Baltimore Orioles"); const bos = records.find((r) => r.teamName === "Boston Red Sox"); expect(ori?.leagueRank).toBe(1); expect(bos?.leagueRank).toBeGreaterThan(1); }); it("never sets otLosses (MLB has no OT losses)", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLB_RESPONSE, } as Response); const adapter = new MlbStandingsAdapter(); const records = await adapter.fetchStandings(); for (const record of records) { expect(record.otLosses).toBeUndefined(); } }); it("uses team numeric id as externalTeamId", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_MLB_RESPONSE, } as Response); const adapter = new MlbStandingsAdapter(); const records = await adapter.fetchStandings(); const ori = records.find((r) => r.teamName === "Baltimore Orioles"); expect(ori?.externalTeamId).toBe("110"); }); it("throws on non-ok HTTP response", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: false, status: 503, statusText: "Service Unavailable", } as Response); const adapter = new MlbStandingsAdapter(); await expect(adapter.fetchStandings()).rejects.toThrow("503"); }); it("throws on unexpected response shape", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ unexpected: true }), } as Response); const adapter = new MlbStandingsAdapter(); await expect(adapter.fetchStandings()).rejects.toThrow(); }); });