brackt/app/services/standings-sync/__tests__/mlb.test.ts
Chris Parsons 08e93e955a
Fix MLB streak doubling bug, hide empty standings columns, improve mobile layout (#413)
* Fix MLB streak doubling bug, hide empty standings columns, improve mobile layout

- mlb.ts: use streakCode alone (the API already returns the full string like
  "W3"); appending streakNumber was doubling the digit, causing "L33" display
- Update mlb.test.ts mocks to match real API format (streakCode "W3" not "W")
- RegularSeasonStandings: conditionally hide GB/L10/STK columns when no rows
  have data, so pre-season or stats-free sports don't show blank columns
- Mobile two-row layout: GP/PCT/GB/L10 move to a secondary sub-row (sm:hidden)
  so all data stays visible without horizontal scroll on small screens; STK and
  W/L remain on the primary row; reduce min-w from 740px to 360px

https://claude.ai/code/session_01RADi3LhYMPbRDm5no1ZpdF

* Address code review: cleanup mlb streak type, fix soccer GP on mobile, hoist showSubRow

- mlb.ts: drop redundant `?? undefined` after optional chain; document that
  streakCode is the full string (e.g. "W3") and streakNumber is unused
- RegularSeasonStandings: hoist hasSecondaryStats → showSubRow to component
  level (it only depends on a prop, not on individual row data)
- Remove non-functional `truncate`/`min-w-0` from team name cell — truncation
  requires table-layout:fixed which we don't use; team names size naturally
- Soccer GP was hidden on mobile with no sub-row to surface it; GP now shows
  inline for soccer on all viewports, hidden only for non-soccer (which has
  the secondary sub-row)
- Add comment explaining totalCols counts hidden-on-mobile columns for colSpan
- Add missing test for GB column hiding when no rows have gamesBack data

https://claude.ai/code/session_01RADi3LhYMPbRDm5no1ZpdF

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-12 08:14:34 -07:00

281 lines
8.4 KiB
TypeScript

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();
});
});