brackt/app/services/standings-sync/__tests__/f1.test.ts
Chris Parsons ea85a21c83
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m0s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m31s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
🚀 Deploy / 🧪 Test (push) Successful in 3m1s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m33s
🚀 Deploy / 🐳 Build (push) Successful in 1m20s
🚀 Deploy / 🚀 Deploy (push) Successful in 11s
Fix IndyCar standings: correct ESPN stat key, update title, restore finalized badge
- IndyCar points were showing as 0 because ESPN uses 'championshipPts' not 'points'
  as the stat name; add it as primary key in the fallback chain
- Rename season_standings card title from sportSeasonName to "${name} Standings"
- Remove non-finalized subtext from SeasonStandings CardDescription
- Restore finalized-season badge (season complete / top-8 points locked) which
  was dropped when removing the subtext; derive from sportsSeason.status at the
  component level instead of the loader so the dead seasonIsFinalized field is
  also removed from the loader return

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 09:17:23 -07:00

260 lines
7.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { F1StandingsAdapter, IndyCarStandingsAdapter } from "../f1";
// Minimal Jolpica driver standings response
const SAMPLE_JOLPICA_STANDINGS = {
MRData: {
StandingsTable: {
StandingsLists: [
{
DriverStandings: [
{
position: "1",
points: "195",
wins: "5",
Driver: { driverId: "max_verstappen", givenName: "Max", familyName: "Verstappen" },
},
{
position: "2",
points: "152",
wins: "2",
Driver: { driverId: "norris", givenName: "Lando", familyName: "Norris" },
},
{
position: "3",
points: "133",
wins: "1",
Driver: { driverId: "leclerc", givenName: "Charles", familyName: "Leclerc" },
},
],
},
],
},
},
};
// Minimal ESPN IndyCar standings response (flat, single standings list)
const SAMPLE_ESPN_INDYCAR = {
standings: {
entries: [
{
athlete: { id: "3097", displayName: "Scott Dixon" },
stats: [
{ name: "rank", value: 1 },
{ name: "championshipPts", value: 437 },
{ name: "wins", value: 3 },
{ name: "starts", value: 10 },
],
},
{
athlete: { id: "3098", displayName: "Alex Palou" },
stats: [
{ name: "rank", value: 2 },
{ name: "championshipPts", value: 398 },
{ name: "wins", value: 2 },
{ name: "starts", value: 10 },
],
},
],
},
};
// IndyCar response nested under children (alternate ESPN shape)
const SAMPLE_ESPN_INDYCAR_NESTED = {
children: [
{
standings: {
entries: SAMPLE_ESPN_INDYCAR.standings.entries,
},
},
],
};
describe("F1StandingsAdapter", () => {
beforeEach(() => {
vi.stubGlobal("fetch", vi.fn());
});
it("returns one record per driver with correct names", async () => {
vi.mocked(fetch)
.mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_JOLPICA_STANDINGS } as Response);
const adapter = new F1StandingsAdapter();
const records = await adapter.fetchStandings();
expect(records).toHaveLength(3);
expect(records[0].teamName).toBe("Max Verstappen");
expect(records[1].teamName).toBe("Lando Norris");
});
it("sets externalTeamId to driverId", async () => {
vi.mocked(fetch)
.mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_JOLPICA_STANDINGS } as Response);
const records = await new F1StandingsAdapter().fetchStandings();
expect(records[0].externalTeamId).toBe("max_verstappen");
expect(records[2].externalTeamId).toBe("leclerc");
});
it("maps championship points to currentPoints", async () => {
vi.mocked(fetch)
.mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_JOLPICA_STANDINGS } as Response);
const records = await new F1StandingsAdapter().fetchStandings();
expect(records[0].currentPoints).toBe(195);
expect(records[1].currentPoints).toBe(152);
});
it("sets leagueRank to championship position", async () => {
vi.mocked(fetch)
.mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_JOLPICA_STANDINGS } as Response);
const records = await new F1StandingsAdapter().fetchStandings();
expect(records[0].leagueRank).toBe(1);
expect(records[2].leagueRank).toBe(3);
});
it("sets gamesPlayed to 0 (not stored for season_standings sports)", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => SAMPLE_JOLPICA_STANDINGS,
} as Response);
const records = await new F1StandingsAdapter().fetchStandings();
expect(records[0].gamesPlayed).toBe(0);
expect(records[1].gamesPlayed).toBe(0);
});
it("sets winPct to 0 (not stored for season_standings sports)", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => SAMPLE_JOLPICA_STANDINGS,
} as Response);
const records = await new F1StandingsAdapter().fetchStandings();
expect(records[0].winPct).toBe(0);
});
it("throws when standings fetch fails", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: false,
status: 500,
statusText: "Internal Server Error",
} as Response);
await expect(new F1StandingsAdapter().fetchStandings()).rejects.toThrow("500");
});
it("throws when API returns empty standings", async () => {
const empty = { MRData: { StandingsTable: { StandingsLists: [] } } };
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => empty,
} as Response);
await expect(new F1StandingsAdapter().fetchStandings()).rejects.toThrow(/no entries/i);
});
});
describe("IndyCarStandingsAdapter", () => {
beforeEach(() => {
vi.stubGlobal("fetch", vi.fn());
});
it("parses flat ESPN standings", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => SAMPLE_ESPN_INDYCAR,
} as Response);
const records = await new IndyCarStandingsAdapter().fetchStandings();
expect(records).toHaveLength(2);
expect(records[0].teamName).toBe("Scott Dixon");
expect(records[1].teamName).toBe("Alex Palou");
});
it("parses nested ESPN standings (children[0].standings)", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => SAMPLE_ESPN_INDYCAR_NESTED,
} as Response);
const records = await new IndyCarStandingsAdapter().fetchStandings();
expect(records).toHaveLength(2);
expect(records[0].teamName).toBe("Scott Dixon");
});
it("sets externalTeamId to ESPN athlete id", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => SAMPLE_ESPN_INDYCAR,
} as Response);
const records = await new IndyCarStandingsAdapter().fetchStandings();
expect(records[0].externalTeamId).toBe("3097");
});
it("maps championship points to currentPoints", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => SAMPLE_ESPN_INDYCAR,
} as Response);
const records = await new IndyCarStandingsAdapter().fetchStandings();
expect(records[0].currentPoints).toBe(437);
expect(records[1].currentPoints).toBe(398);
});
it("sets leagueRank from rank stat", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => SAMPLE_ESPN_INDYCAR,
} as Response);
const records = await new IndyCarStandingsAdapter().fetchStandings();
expect(records[0].leagueRank).toBe(1);
expect(records[1].leagueRank).toBe(2);
});
it("sets gamesPlayed from starts stat", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => SAMPLE_ESPN_INDYCAR,
} as Response);
const records = await new IndyCarStandingsAdapter().fetchStandings();
expect(records[0].gamesPlayed).toBe(10);
});
it("computes winPct as wins / starts", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => SAMPLE_ESPN_INDYCAR,
} as Response);
const records = await new IndyCarStandingsAdapter().fetchStandings();
expect(records[0].winPct).toBeCloseTo(3 / 10, 5);
});
it("throws when API returns HTTP error", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: false,
status: 404,
statusText: "Not Found",
} as Response);
await expect(new IndyCarStandingsAdapter().fetchStandings()).rejects.toThrow("404");
});
it("throws when API returns no entries", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ standings: { entries: [] } }),
} as Response);
await expect(new IndyCarStandingsAdapter().fetchStandings()).rejects.toThrow(
/no entries/i
);
});
});