Fix QP standings: filter to drafted/points-earning participants with correct global ranks
Some checks failed
🚀 Deploy / 🧪 Test (pull_request) Failing after 2m33s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m20s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped

- Filter the QP table to show only participants with QP > 0 or drafted by any team in the league (undrafted 0-QP participants hidden)
- Compute global ranks with tie handling across the full field before filtering so displayed rank numbers and the top-8 Points Line remain correct after rows are removed
- Fix flaky CI timeouts: world-cup simulator test (100 → 50 iterations), SportsSection userEvent test (explicit 15s timeout)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-06-06 09:38:47 -07:00
parent 079ecec129
commit 73f0256ff5
7 changed files with 50 additions and 27 deletions

View file

@ -23,6 +23,7 @@ describe("QualifyingPointsStandings", () => {
totalQualifyingPoints: "14.67",
eventsScored: 1,
finalRanking: null,
globalRank: 1,
participant: { id: "participant-1", name: "Player One" },
},
{
@ -30,6 +31,7 @@ describe("QualifyingPointsStandings", () => {
totalQualifyingPoints: "0.50",
eventsScored: 1,
finalRanking: null,
globalRank: 2,
participant: { id: "participant-2", name: "Player Two" },
},
{
@ -37,6 +39,7 @@ describe("QualifyingPointsStandings", () => {
totalQualifyingPoints: "0.43",
eventsScored: 1,
finalRanking: null,
globalRank: 3,
participant: { id: "participant-3", name: "Player Three" },
},
]}

View file

@ -82,7 +82,7 @@ describe("SportsSection", () => {
await user.click(screen.getByText("2025 MLB Season"));
expect(mlbCheckbox).toBeChecked();
});
}, 15000);
it("submits the currently selected sports seasons after toggling", async () => {
const user = userEvent.setup();

View file

@ -23,6 +23,7 @@ interface QPStanding {
totalQualifyingPoints: string;
eventsScored: number;
finalRanking: number | null;
globalRank: number;
participant: {
id: string;
name: string;
@ -70,26 +71,11 @@ export function QualifyingPointsStandings({
const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o]));
const userParticipantSet = new Set(userParticipantIds);
// Assign current ranks with tie handling
const rankedStandings: Array<QPStanding & { currentRank: number }> = [];
let previousQP = -1;
let previousRankStart = -1;
for (let index = 0; index < standings.length; index++) {
const standing = standings[index];
const currentQP = parseFloat(standing.totalQualifyingPoints);
let currentRank: number;
if (index > 0 && Math.abs(currentQP - previousQP) < 0.001) {
currentRank = previousRankStart;
} else {
currentRank = index + 1;
}
rankedStandings.push({ ...standing, currentRank });
previousRankStart = currentRank;
previousQP = currentQP;
}
// Use the pre-computed global rank from the loader so displayed ranks reflect the full
// participant field even when the array has been filtered down to drafted/points-earning rows.
const rankedStandings: Array<QPStanding & { currentRank: number }> = standings.map(
(standing) => ({ ...standing, currentRank: standing.globalRank })
);
const tiedCountByRank = new Map<number, number>();
for (const s of rankedStandings) {

View file

@ -64,6 +64,7 @@ interface QPStanding {
totalQualifyingPoints: string;
eventsScored: number;
finalRanking: number | null;
globalRank: number;
participant: {
id: string;
name: string;

View file

@ -25,11 +25,25 @@ export async function loader({ params }: Route.LoaderArgs) {
const events = await getScoringEventsForSportsSeason(params.id);
// For qualifying sports seasons, get QP standings
// For qualifying sports seasons, get QP standings with global ranks attached
let qpStandings = null;
const scoringRules = null;
if (sportsSeason.scoringPattern === "qualifying_points") {
qpStandings = await getQPStandings(params.id);
const standings = await getQPStandings(params.id);
let prevQP = -1;
let prevRankStart = 1;
qpStandings = standings.map((s, index) => {
const qp = parseFloat(s.totalQualifyingPoints);
let rank: number;
if (index > 0 && Math.abs(qp - prevQP) < 0.001) {
rank = prevRankStart;
} else {
rank = index + 1;
prevRankStart = rank;
}
prevQP = qp;
return { ...s, globalRank: rank };
});
}
// Tournaments for this sport that don't already have a scoring event in this season.

View file

@ -155,7 +155,7 @@ export async function loader(args: Route.LoaderArgs) {
let participantPoints: { participantId: string; points: number }[] = [];
let partialScoreParticipantIds: string[] = [];
let seasonStandings: SeasonStanding[] = [];
type QPStanding = Awaited<ReturnType<typeof getQPStandings>>[number];
type QPStanding = Awaited<ReturnType<typeof getQPStandings>>[number] & { globalRank: number };
let qpStandings: QPStanding[] = [];
// Group standings for group-stage events (e.g. FIFA World Cup)
@ -293,9 +293,28 @@ export async function loader(args: Route.LoaderArgs) {
},
}));
} else if (scoringPattern === "qualifying_points") {
// Fetch qualifying points standings
const standings = await getQPStandings(sportsSeasonId);
qpStandings = standings;
// Compute global ranks with tie handling across the full field before filtering,
// so the displayed rank numbers remain correct after undrafted/zero-QP rows are removed.
let prevQP = -1;
let prevRankStart = 1;
const withGlobalRank = standings.map((s, index) => {
const qp = parseFloat(s.totalQualifyingPoints);
let rank: number;
if (index > 0 && Math.abs(qp - prevQP) < 0.001) {
rank = prevRankStart;
} else {
rank = index + 1;
prevRankStart = rank;
}
prevQP = qp;
return { ...s, globalRank: rank };
});
qpStandings = withGlobalRank.filter(
(s) =>
parseFloat(s.totalQualifyingPoints) > 0 ||
ownershipMap.has(s.participant.id)
);
}
// Fetch event schedule for all patterns (upcoming + recent)

View file

@ -162,7 +162,7 @@ describe("WorldCupSimulator", () => {
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
const sim = new WorldCupSimulator(100);
const sim = new WorldCupSimulator(50);
const results = await sim.simulate("season-1");
// probFirst + probSecond + probThird + probFourth should cover all probability mass