brackt/app/models/__tests__/standings-snapshots.test.ts
Chris Parsons e2b178221a
Add oxlint linting setup with zero errors (#194)
* Add oxlint and fix all lint errors

- Install oxlint, add .oxlintrc.json with rules for TypeScript/React
- Add npm run lint / lint:fix scripts
- Add Claude PostToolUse hook to run oxlint on every edited file
- Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array
- Fix no-array-index-key (use stable keys or suppress positional cases)
- Fix exhaustive-deps missing dependency in useEffect
- Promote exhaustive-deps and no-array-index-key to errors
- Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix no-explicit-any warnings and upgrade tsconfig to ES2023

- Replace all `any` types with proper types or `unknown` across ~20 files
- Add typed socket payload interfaces in draft route and useDraftSocket
- Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch)
- Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed
- Fix cascading type errors uncovered by removing any: Map.get narrowing,
  participant relation types, ChartDataPoint, Partial<NewSeason> indexing
- Add ParticipantResultWithParticipant type to participant-result model
- Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult)
- Fix duplicate getQPStandings import in sportsSeasonId.server.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Promote no-explicit-any to error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:44:05 -07:00

202 lines
6.8 KiB
TypeScript

import { describe, it, expect } from "vitest";
/**
* Standings Snapshots Tests
* Phase 4.2: Test snapshot creation and 7-day comparison logic
*
* These are unit tests that verify the snapshot logic works correctly.
* Integration tests that use the actual database are in the e2e test suite.
*/
/**
* Helper function to simulate 7-day rank change calculation
* This mirrors the logic in getSevenDayStandingsChange
*/
function calculateSevenDayChange(
currentRank: number,
sevenDayOldRank: number | null
): number {
if (sevenDayOldRank === null) {
return 0; // No snapshot from 7 days ago
}
// Positive number = improvement (was 5th, now 3rd = +2)
// Negative number = decline (was 3rd, now 5th = -2)
return sevenDayOldRank - currentRank;
}
/**
* Helper function to simulate snapshot deduplication logic
* This mirrors the logic in createDailySnapshot
*/
function shouldCreateSnapshot(
existingSnapshotDate: string | null,
today: string
): boolean {
// Don't create if snapshot already exists for today
return existingSnapshotDate !== today;
}
describe("Standings Snapshots", () => {
describe("7-Day Rank Change Calculation", () => {
it("should calculate positive change when rank improves", () => {
const change = calculateSevenDayChange(1, 3);
expect(change).toBe(2); // Moved from 3rd to 1st = +2
});
it("should calculate negative change when rank declines", () => {
const change = calculateSevenDayChange(5, 2);
expect(change).toBe(-3); // Moved from 2nd to 5th = -3
});
it("should return 0 when rank stays the same", () => {
const change = calculateSevenDayChange(2, 2);
expect(change).toBe(0);
});
it("should return 0 when no snapshot exists from 7 days ago", () => {
const change = calculateSevenDayChange(1, null);
expect(change).toBe(0);
});
it("should handle large rank improvements", () => {
const change = calculateSevenDayChange(1, 10);
expect(change).toBe(9); // Moved from 10th to 1st = +9
});
it("should handle large rank declines", () => {
const change = calculateSevenDayChange(10, 1);
expect(change).toBe(-9); // Moved from 1st to 10th = -9
});
});
describe("Snapshot Deduplication", () => {
it("should create snapshot when no existing snapshot for today", () => {
const today = "2025-01-13";
const shouldCreate = shouldCreateSnapshot(null, today);
expect(shouldCreate).toBe(true);
});
it("should not create snapshot when snapshot already exists for today", () => {
const today = "2025-01-13";
const existingDate = "2025-01-13";
const shouldCreate = shouldCreateSnapshot(existingDate, today);
expect(shouldCreate).toBe(false);
});
it("should create snapshot when last snapshot was yesterday", () => {
const today = "2025-01-13";
const existingDate = "2025-01-12";
const shouldCreate = shouldCreateSnapshot(existingDate, today);
expect(shouldCreate).toBe(true);
});
it("should create snapshot when last snapshot was 7 days ago", () => {
const today = "2025-01-13";
const existingDate = "2025-01-06";
const shouldCreate = shouldCreateSnapshot(existingDate, today);
expect(shouldCreate).toBe(true);
});
});
describe("Snapshot Data Integrity", () => {
it("should preserve all tiebreaker data in snapshot", () => {
const standing = {
totalPoints: 150.5,
currentRank: 1,
firstPlaceCount: 2,
secondPlaceCount: 1,
thirdPlaceCount: 0,
fourthPlaceCount: 0,
fifthPlaceCount: 0,
sixthPlaceCount: 0,
seventhPlaceCount: 0,
eighthPlaceCount: 0,
participantsRemaining: 5,
};
// Snapshot should contain all the same data
const snapshot = {
totalPoints: standing.totalPoints,
rank: standing.currentRank,
firstPlaceCount: standing.firstPlaceCount,
secondPlaceCount: standing.secondPlaceCount,
thirdPlaceCount: standing.thirdPlaceCount,
fourthPlaceCount: standing.fourthPlaceCount,
fifthPlaceCount: standing.fifthPlaceCount,
sixthPlaceCount: standing.sixthPlaceCount,
seventhPlaceCount: standing.seventhPlaceCount,
eighthPlaceCount: standing.eighthPlaceCount,
participantsRemaining: standing.participantsRemaining,
};
expect(snapshot.totalPoints).toBe(standing.totalPoints);
expect(snapshot.rank).toBe(standing.currentRank);
expect(snapshot.firstPlaceCount).toBe(standing.firstPlaceCount);
expect(snapshot.participantsRemaining).toBe(standing.participantsRemaining);
});
});
describe("Movement Scenarios", () => {
it("should handle team moving from last to first", () => {
const scenarios = [
{ current: 1, old: 8, expected: 7 }, // Last to first = +7
{ current: 8, old: 1, expected: -7 }, // First to last = -7
];
scenarios.forEach(({ current, old, expected }) => {
const change = calculateSevenDayChange(current, old);
expect(change).toBe(expected);
});
});
it("should handle multiple teams with different movements", () => {
const teams = [
{ id: "A", currentRank: 1, oldRank: 3, expectedChange: 2 },
{ id: "B", currentRank: 2, oldRank: 1, expectedChange: -1 },
{ id: "C", currentRank: 3, oldRank: 2, expectedChange: -1 },
{ id: "D", currentRank: 4, oldRank: null, expectedChange: 0 },
];
teams.forEach((team) => {
const change = calculateSevenDayChange(team.currentRank, team.oldRank);
expect(change).toBe(team.expectedChange);
});
});
});
describe("Historical Tracking", () => {
it("should maintain chronological order of snapshots", () => {
const snapshots = [
{ date: "2025-01-13", rank: 1 },
{ date: "2025-01-12", rank: 2 },
{ date: "2025-01-11", rank: 3 },
{ date: "2025-01-10", rank: 2 },
{ date: "2025-01-09", rank: 1 },
];
// Should be sorted by date descending (newest first)
const sorted = [...snapshots].toSorted((a, b) =>
b.date.localeCompare(a.date)
);
expect(sorted[0].date).toBe("2025-01-13");
expect(sorted[4].date).toBe("2025-01-09");
});
it("should show rank progression over time", () => {
const history = [
{ date: "2025-01-09", rank: 5, points: 50 },
{ date: "2025-01-10", rank: 4, points: 75 },
{ date: "2025-01-11", rank: 3, points: 100 },
{ date: "2025-01-12", rank: 2, points: 125 },
{ date: "2025-01-13", rank: 1, points: 150 },
];
// Verify progression shows improvement
for (let i = 1; i < history.length; i++) {
expect(history[i].rank).toBeLessThan(history[i - 1].rank);
expect(history[i].points).toBeGreaterThan(history[i - 1].points);
}
});
});
});