brackt/app/models/__tests__/qualifying-points.test.ts
Chris Parsons 1089022c09 feat: Implement Qualifying Points Standings component and related logic
- Added `QualifyingPointsStandings` component to display standings based on qualifying points.
- Introduced scoring rules and projected points calculation for participants.
- Implemented tie handling for rankings and displayed appropriate UI elements based on finalization status.
- Created tests for qualifying points configuration, accumulation logic, ranking logic, and scoring workflow.
- Developed scoring calculator tests to validate fantasy points conversion from qualifying points.
- Established qualifying points management functions including initialization, retrieval, updating, and resetting of points.
2025-11-11 10:08:25 -08:00

339 lines
11 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { DEFAULT_QP_VALUES } from "../qualifying-points";
describe("Qualifying Points Configuration", () => {
describe("DEFAULT_QP_VALUES", () => {
it("should have 16 placement values", () => {
expect(DEFAULT_QP_VALUES).toHaveLength(16);
});
it("should have correct QP values for top placements", () => {
expect(DEFAULT_QP_VALUES[0]).toEqual({ placement: 1, points: 20 });
expect(DEFAULT_QP_VALUES[1]).toEqual({ placement: 2, points: 14 });
expect(DEFAULT_QP_VALUES[2]).toEqual({ placement: 3, points: 10 });
expect(DEFAULT_QP_VALUES[3]).toEqual({ placement: 4, points: 8 });
});
it("should have 5 QP for 5th and 6th place", () => {
expect(DEFAULT_QP_VALUES[4]).toEqual({ placement: 5, points: 5 });
expect(DEFAULT_QP_VALUES[5]).toEqual({ placement: 6, points: 5 });
});
it("should have 3 QP for 7th and 8th place", () => {
expect(DEFAULT_QP_VALUES[6]).toEqual({ placement: 7, points: 3 });
expect(DEFAULT_QP_VALUES[7]).toEqual({ placement: 8, points: 3 });
});
it("should have 2 QP for 9th-12th place", () => {
expect(DEFAULT_QP_VALUES[8]).toEqual({ placement: 9, points: 2 });
expect(DEFAULT_QP_VALUES[9]).toEqual({ placement: 10, points: 2 });
expect(DEFAULT_QP_VALUES[10]).toEqual({ placement: 11, points: 2 });
expect(DEFAULT_QP_VALUES[11]).toEqual({ placement: 12, points: 2 });
});
it("should have 1 QP for 13th-16th place", () => {
expect(DEFAULT_QP_VALUES[12]).toEqual({ placement: 13, points: 1 });
expect(DEFAULT_QP_VALUES[13]).toEqual({ placement: 14, points: 1 });
expect(DEFAULT_QP_VALUES[14]).toEqual({ placement: 15, points: 1 });
expect(DEFAULT_QP_VALUES[15]).toEqual({ placement: 16, points: 1 });
});
it("should be sorted by placement (ascending)", () => {
for (let i = 0; i < DEFAULT_QP_VALUES.length - 1; i++) {
expect(DEFAULT_QP_VALUES[i].placement).toBeLessThan(
DEFAULT_QP_VALUES[i + 1].placement
);
}
});
it("should have descending point values for higher placements", () => {
expect(DEFAULT_QP_VALUES[0].points).toBeGreaterThan(DEFAULT_QP_VALUES[1].points);
expect(DEFAULT_QP_VALUES[1].points).toBeGreaterThan(DEFAULT_QP_VALUES[2].points);
expect(DEFAULT_QP_VALUES[2].points).toBeGreaterThan(DEFAULT_QP_VALUES[3].points);
});
});
describe("QP Accumulation Logic", () => {
it("should accumulate QP across multiple events", () => {
// Simulating participant earning QP from 4 majors
const event1QP = 20; // 1st place
const event2QP = 14; // 2nd place
const event3QP = 10; // 3rd place
const event4QP = 8; // 4th place
const totalQP = event1QP + event2QP + event3QP + event4QP;
expect(totalQP).toBe(52);
});
it("should handle decimal QP values", () => {
const qp1 = 20.5;
const qp2 = 14.25;
const total = qp1 + qp2;
expect(total).toBe(34.75);
});
it("should handle zero QP awards", () => {
const event1QP = 20;
const event2QP = 0; // Finished beyond 16th
const total = event1QP + event2QP;
expect(total).toBe(20);
});
});
describe("QP Ranking Logic", () => {
it("should rank participants by total QP (highest first)", () => {
const standings = [
{ name: "Player A", qp: 44 },
{ name: "Player B", qp: 50 },
{ name: "Player C", qp: 38 },
];
const sorted = standings.sort((a, b) => b.qp - a.qp);
expect(sorted[0].name).toBe("Player B"); // 50 QP
expect(sorted[1].name).toBe("Player A"); // 44 QP
expect(sorted[2].name).toBe("Player C"); // 38 QP
});
it("should handle ties in QP totals", () => {
const standings = [
{ name: "Player A", qp: 30 },
{ name: "Player B", qp: 30 },
{ name: "Player C", qp: 20 },
];
const sorted = standings.sort((a, b) => b.qp - a.qp);
// Both players with 30 QP should be before Player C
expect(sorted[2].name).toBe("Player C");
expect(sorted[0].qp).toBe(30);
expect(sorted[1].qp).toBe(30);
});
});
describe("QP to Fantasy Placement Conversion", () => {
it("should assign top 8 QP leaders to fantasy placements 1-8", () => {
const qpRankings = [50, 44, 38, 30, 25, 20, 15, 10, 5, 2];
const top8 = qpRankings.slice(0, 8);
expect(top8).toHaveLength(8);
expect(top8[0]).toBe(50); // 1st place
expect(top8[7]).toBe(10); // 8th place
});
it("should assign 0 fantasy points to participants beyond top 8", () => {
const qpRankings = [50, 44, 38, 30, 25, 20, 15, 10, 5, 2];
const beyond8 = qpRankings.slice(8);
expect(beyond8).toHaveLength(2);
// These would get 0 fantasy points
const fantasyPoints = beyond8.map(() => 0);
expect(fantasyPoints).toEqual([0, 0]);
});
it("should handle ties at the cutoff (8th place)", () => {
// Scenario: Two players tied for 8th place QP
const qpRankings = [
{ name: "P1", qp: 50 }, // 1st
{ name: "P2", qp: 44 }, // 2nd
{ name: "P3", qp: 38 }, // 3rd
{ name: "P4", qp: 30 }, // 4th
{ name: "P5", qp: 25 }, // 5th
{ name: "P6", qp: 20 }, // 6th
{ name: "P7", qp: 15 }, // 7th
{ name: "P8", qp: 10 }, // Tied for 8th
{ name: "P9", qp: 10 }, // Tied for 8th
];
// Both should get placement 8
const tied8th = qpRankings.filter(p => p.qp === 10);
expect(tied8th).toHaveLength(2);
// In implementation, both would share 8th place fantasy placement
const placement = 8;
expect(placement).toBe(8);
});
});
describe("Tie Handling During QP Awards", () => {
it("should correctly split QP among tied participants", () => {
// Scenario: 7 players tied for 14th place
// They occupy positions 14-20
// Positions 14, 15, 16 get 1 QP each = 3 total QP
// Positions 17-20 get 0 QP
// Total: 3 QP divided by 7 players = 3/7 = 0.428571... per player
const tieCount = 7;
const startPosition = 14;
// Calculate total QP for positions 14-20
const qpMap: Record<number, number> = {
14: 1,
15: 1,
16: 1,
17: 0,
18: 0,
19: 0,
20: 0,
};
let totalQP = 0;
for (let pos = startPosition; pos < startPosition + tieCount; pos++) {
totalQP += qpMap[pos] || 0;
}
const qpPerPlayer = totalQP / tieCount;
expect(totalQP).toBe(3);
expect(qpPerPlayer).toBeCloseTo(0.428571, 5);
});
it("should handle 2-way tie for 5th place", () => {
// 2 players tied for 5th occupy positions 5 and 6
// Position 5: 5 QP, Position 6: 5 QP
// Total: 10 QP / 2 players = 5 QP each
const tieCount = 2;
const startPosition = 5;
const qpMap: Record<number, number> = { 5: 5, 6: 5 };
let totalQP = 0;
for (let pos = startPosition; pos < startPosition + tieCount; pos++) {
totalQP += qpMap[pos];
}
const qpPerPlayer = totalQP / tieCount;
expect(totalQP).toBe(10);
expect(qpPerPlayer).toBe(5);
});
it("should handle 3-way tie for 1st place", () => {
// 3 players tied for 1st occupy positions 1, 2, 3
// Position 1: 20 QP, Position 2: 14 QP, Position 3: 10 QP
// Total: 44 QP / 3 players = 14.666... QP each
const tieCount = 3;
const startPosition = 1;
const qpMap: Record<number, number> = { 1: 20, 2: 14, 3: 10 };
let totalQP = 0;
for (let pos = startPosition; pos < startPosition + tieCount; pos++) {
totalQP += qpMap[pos];
}
const qpPerPlayer = totalQP / tieCount;
expect(totalQP).toBe(44);
expect(qpPerPlayer).toBeCloseTo(14.666667, 5);
});
});
describe("Reprocessing Events", () => {
it("should handle reprocessing by removing old QP before adding new QP", () => {
// Scenario: Admin enters results, processes QP, then realizes there's an error
// They fix the placements and reprocess
// Initial processing
const participant1InitialQP = 20; // 1st place
const participant2InitialQP = 14; // 2nd place
let totalQP1 = participant1InitialQP;
let totalQP2 = participant2InitialQP;
// Admin realizes participant 1 should have been 3rd, not 1st
// Reprocessing: subtract old QP, add new QP
totalQP1 = totalQP1 - participant1InitialQP + 10; // Remove 20, add 10 (3rd place)
totalQP2 = totalQP2 - participant2InitialQP + 20; // Remove 14, add 20 (moved to 1st)
expect(totalQP1).toBe(10); // Should have 10 QP (3rd place)
expect(totalQP2).toBe(20); // Should have 20 QP (1st place)
});
it("should not increment majorsCompleted when reprocessing", () => {
// First processing
let majorsCompleted = 0;
const wasAlreadyProcessed = false;
if (!wasAlreadyProcessed) {
majorsCompleted += 1;
}
expect(majorsCompleted).toBe(1);
// Reprocessing (wasAlreadyProcessed = true)
const reprocessing = true;
if (!reprocessing) {
majorsCompleted += 1;
}
expect(majorsCompleted).toBe(1); // Should still be 1, not 2
});
});
describe("Scoring Workflow", () => {
it("should process 4 majors and calculate final standings", () => {
// Simulating a full season with 4 majors
const participants = [
{ name: "Tiger Woods", qp: 0, events: 0 },
{ name: "Rory McIlroy", qp: 0, events: 0 },
{ name: "Scottie Scheffler", qp: 0, events: 0 },
];
// Event 1: The Masters
participants[0].qp += 20; // Tiger wins
participants[0].events++;
participants[1].qp += 14; // Rory 2nd
participants[1].events++;
participants[2].qp += 10; // Scottie 3rd
participants[2].events++;
// Event 2: PGA Championship
participants[1].qp += 20; // Rory wins
participants[1].events++;
participants[0].qp += 14; // Tiger 2nd
participants[0].events++;
participants[2].qp += 10; // Scottie 3rd
participants[2].events++;
// Event 3: U.S. Open
participants[2].qp += 20; // Scottie wins
participants[2].events++;
participants[0].qp += 14; // Tiger 2nd
participants[0].events++;
participants[1].qp += 10; // Rory 3rd
participants[1].events++;
// Event 4: The Open
participants[0].qp += 20; // Tiger wins
participants[0].events++;
participants[1].qp += 14; // Rory 2nd
participants[1].events++;
participants[2].qp += 10; // Scottie 3rd
participants[2].events++;
// Final standings
participants.sort((a, b) => b.qp - a.qp);
expect(participants[0].name).toBe("Tiger Woods");
expect(participants[0].qp).toBe(68); // 20+14+14+20
expect(participants[0].events).toBe(4);
expect(participants[1].name).toBe("Rory McIlroy");
expect(participants[1].qp).toBe(58); // 14+20+10+14
expect(participants[1].events).toBe(4);
expect(participants[2].name).toBe("Scottie Scheffler");
expect(participants[2].qp).toBe(50); // 10+10+20+10
expect(participants[2].events).toBe(4);
});
});
});