The user asked whether the DNS fix had actually been tested against the different points systems, not just the default one. It hadn't - buildQualifyingStandings had never been exercised end to end with qualifyingPointsTable "field_desc"/"ifmar" (high-points-win scoring) or qualifyingScoring "best" (ranked by time, not points) at all, DNS or otherwise. Added two tests: a DNS driver scored under the high-points field_desc table (confirms the DNS row still ranks last under an inverted sort direction, and isn't scored as either zero or the winner's points), and one under "best time" qualifying (confirms a bad DNS heat can't contaminate a driver's genuine best result, since bestRank/ bestRoundMetric take the driver's single best heat, not an average). Both passed immediately, confirming the fix already handled these correctly - this closes a real, previously-untested gap in confidence rather than fixing a new bug. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
829 lines
37 KiB
JavaScript
829 lines
37 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
applyBumpsForRace,
|
|
buildOpenPracticeBundle,
|
|
buildRaceFormatConfigFromForm,
|
|
buildRaceSessionsFromWizard,
|
|
generateFinalsForRace,
|
|
generateQualifyingForRace,
|
|
getTeamDriverPool,
|
|
normalizeEvent,
|
|
reassignTeamMembers,
|
|
reseedUpcomingQualifying,
|
|
} from "../src/event_race_logic.js";
|
|
import {
|
|
buildFinalStandings,
|
|
buildLeaderboard,
|
|
buildQualifyingStandings,
|
|
buildQualifyingTieBreakNote as buildQualifyingTieBreakNoteLogic,
|
|
buildSessionChannelStats,
|
|
compareNumberSet,
|
|
formatLeaderboardGap,
|
|
getCompetitorElapsedMs,
|
|
getCompetitorSeedMetric,
|
|
getQualifyingPointsValue,
|
|
getSessionLapWindow,
|
|
getSessionTargetMs,
|
|
getSessionTiming,
|
|
hasQualifyingPrimaryTie,
|
|
isHighPointsTable,
|
|
isUntimedSession,
|
|
ensureSessionResult,
|
|
} from "../src/timing_logic.js";
|
|
|
|
const testFormatLap = (ms) => (Number.isFinite(ms) ? `${ms}ms` : "-");
|
|
const testFormatRaceClock = (ms) => `${ms}ms`;
|
|
const testTranslate = (key, vars) => (vars ? `${key}:${JSON.stringify(vars)}` : key);
|
|
|
|
function createUid() {
|
|
let index = 0;
|
|
return (prefix) => `${prefix}_${++index}`;
|
|
}
|
|
|
|
function createForm(values) {
|
|
const fields = new Map(Object.entries(values));
|
|
return { get: (name) => fields.get(name) ?? null };
|
|
}
|
|
|
|
const normalizeStartMode = (mode) => ["mass", "staggered", "position"].includes(mode) ? mode : "mass";
|
|
const chunkArray = (items, size) => {
|
|
const chunks = [];
|
|
for (let index = 0; index < items.length; index += size) {
|
|
chunks.push(items.slice(index, index + size));
|
|
}
|
|
return chunks;
|
|
};
|
|
|
|
test("race format form normalizes limits and preserves participants and teams", () => {
|
|
const event = {
|
|
raceConfig: {
|
|
driverIds: ["driver_1", "driver_2"],
|
|
participantsConfigured: true,
|
|
teams: [{ id: "team_1", name: "Red" }],
|
|
},
|
|
};
|
|
const config = buildRaceFormatConfigFromForm(
|
|
createForm({
|
|
presetId: "club_qualifying",
|
|
qualifyingScoring: "best",
|
|
qualifyingRounds: "0",
|
|
carsPerHeat: "1",
|
|
qualDurationMin: "0",
|
|
qualStartMode: "position",
|
|
qualSeedLapCount: "3",
|
|
qualSeedMethod: "invalid",
|
|
countedQualRounds: "2",
|
|
qualifyingPointsTable: "ifmar",
|
|
qualifyingTieBreak: "best_lap",
|
|
carsPerFinal: "1",
|
|
finalLegs: "3",
|
|
countedFinalLegs: "2",
|
|
finalDurationMin: "5",
|
|
finalStartMode: "staggered",
|
|
followUpSec: "15",
|
|
minLapSec: "11.25",
|
|
maxLapSec: "60.5",
|
|
bumpCount: "1",
|
|
reserveBumpSlots: "on",
|
|
finalsSource: "practice",
|
|
}),
|
|
event,
|
|
{ normalizeStartMode, getEventTeams: (item) => item.raceConfig.teams }
|
|
);
|
|
|
|
assert.equal(config.qualifyingRounds, 3);
|
|
assert.equal(config.carsPerHeat, 2);
|
|
assert.equal(config.qualDurationMin, 5);
|
|
assert.equal(config.qualSeedMethod, "best_sum");
|
|
assert.equal(config.carsPerFinal, 2);
|
|
assert.equal(config.minLapMs, 11250);
|
|
assert.equal(config.maxLapMs, 60500);
|
|
assert.equal(config.reserveBumpSlots, true);
|
|
assert.equal(config.finalsSource, "practice");
|
|
assert.deepEqual(config.driverIds, ["driver_1", "driver_2"]);
|
|
assert.deepEqual(config.teams, [{ id: "team_1", name: "Red" }]);
|
|
});
|
|
|
|
test("open practice bundle is untimed, unrestricted, and independent of race presets", () => {
|
|
const uid = createUid();
|
|
const bundle = buildOpenPracticeBundle(
|
|
{ name: "Friday Practice", date: "2026-08-24" },
|
|
{
|
|
uid,
|
|
normalizeEvent: (event) => ({ ...event, raceConfig: { presetId: "club_qualifying", driverIds: ["old"] } }),
|
|
buildRaceSession: (eventId, name, type, durationMin, overrides) => ({ eventId, name, type, durationMin, ...overrides }),
|
|
}
|
|
);
|
|
|
|
assert.equal(bundle.event.name, "Friday Practice");
|
|
assert.equal(bundle.event.classId, "");
|
|
assert.equal(bundle.event.mode, "race");
|
|
assert.equal(bundle.event.raceConfig.presetId, "custom");
|
|
assert.equal(bundle.event.raceConfig.practiceOnly, true);
|
|
assert.equal(bundle.event.raceConfig.participantsConfigured, false);
|
|
assert.deepEqual(bundle.event.raceConfig.driverIds, []);
|
|
assert.equal(bundle.session.eventId, bundle.event.id);
|
|
assert.equal(bundle.session.type, "open_practice");
|
|
assert.equal(bundle.session.startMode, "mass");
|
|
assert.equal(bundle.session.followUpSec, 0);
|
|
assert.deepEqual(bundle.session.driverIds, []);
|
|
assert.equal(isUntimedSession(bundle.session), true);
|
|
});
|
|
|
|
test("event normalization preserves the practice-only marker", () => {
|
|
const event = normalizeEvent(
|
|
{ id: "event_1", raceConfig: { practiceOnly: true } },
|
|
{
|
|
normalizeBrandingConfig: (branding) => branding || {},
|
|
normalizeStartMode,
|
|
normalizeRaceTeam: (team) => team,
|
|
}
|
|
);
|
|
|
|
assert.equal(event.raceConfig.practiceOnly, true);
|
|
assert.equal(event.raceConfig.presetId, "custom");
|
|
});
|
|
|
|
test("reassignTeamMembers strips a moved driver/car from every other team, leaving the excluded team untouched", () => {
|
|
const teams = [
|
|
{ id: "team_a", name: "A", driverIds: ["d1", "d2"], carIds: ["c1"] },
|
|
{ id: "team_b", name: "B", driverIds: ["d3"], carIds: [] },
|
|
];
|
|
const result = reassignTeamMembers(teams, { excludeTeamId: "team_b", driverIds: ["d1"], carIds: ["c1"] });
|
|
const teamA = result.find((team) => team.id === "team_a");
|
|
const teamB = result.find((team) => team.id === "team_b");
|
|
assert.deepEqual(teamA.driverIds, ["d2"], "d1 was moved out of team A");
|
|
assert.deepEqual(teamA.carIds, [], "c1 was moved out of team A too");
|
|
assert.deepEqual(teamB.driverIds, ["d3"], "the team being saved is left for the caller to update, not touched here");
|
|
});
|
|
|
|
test("reassignTeamMembers returns unchanged team objects as-is when nothing overlaps", () => {
|
|
const teams = [{ id: "team_a", name: "A", driverIds: ["d1"], carIds: [] }];
|
|
const result = reassignTeamMembers(teams, { excludeTeamId: "team_b", driverIds: ["d9"], carIds: [] });
|
|
assert.strictEqual(result[0], teams[0]);
|
|
});
|
|
|
|
test("getTeamDriverPool scopes to the event's class/participants, only falling back to every driver when that list is empty", () => {
|
|
const state = {
|
|
drivers: [
|
|
{ id: "d1", name: "One", classId: "2wd" },
|
|
{ id: "d2", name: "Two", classId: "4wd" },
|
|
],
|
|
};
|
|
const getEventDrivers = (event) => state.drivers.filter((driver) => !event?.classId || driver.classId === event.classId);
|
|
|
|
const scoped = getTeamDriverPool({ classId: "2wd", raceConfig: {} }, { getEventDrivers, state });
|
|
assert.deepEqual(scoped.drivers.map((driver) => driver.id), ["d1"]);
|
|
assert.equal(scoped.fallback, false);
|
|
|
|
const noMatches = getTeamDriverPool({ classId: "unknown_class", raceConfig: {} }, { getEventDrivers, state });
|
|
assert.deepEqual(noMatches.drivers.map((driver) => driver.id), ["d1", "d2"], "falls back to every driver rather than leaving zero checkboxes");
|
|
assert.equal(noMatches.fallback, true);
|
|
});
|
|
|
|
test("race wizard creates configured practice, qualifying, and endurance sessions", () => {
|
|
const event = {
|
|
id: "event_1",
|
|
raceConfig: {
|
|
driverIds: ["driver_1", "driver_2"],
|
|
qualDurationMin: 6,
|
|
qualStartMode: "staggered",
|
|
qualSeedLapCount: 3,
|
|
qualSeedMethod: "consecutive",
|
|
finalDurationMin: 240,
|
|
followUpSec: 20,
|
|
},
|
|
};
|
|
let sequence = 0;
|
|
const buildRaceSession = (eventId, name, type, durationMin, overrides) => ({
|
|
id: `session_${++sequence}`,
|
|
eventId,
|
|
name,
|
|
type,
|
|
durationMin,
|
|
...overrides,
|
|
});
|
|
const sessions = buildRaceSessionsFromWizard(
|
|
event,
|
|
{
|
|
createPractice: true,
|
|
practiceSessions: 2,
|
|
createQualifying: true,
|
|
qualifyingRounds: 3,
|
|
createTeamRace: true,
|
|
teamRaceDurationMin: 180,
|
|
},
|
|
{ t: (key) => key, buildRaceSession }
|
|
);
|
|
|
|
assert.equal(sessions.length, 6);
|
|
assert.deepEqual(sessions.map((session) => session.type), [
|
|
"practice",
|
|
"practice",
|
|
"qualification",
|
|
"qualification",
|
|
"qualification",
|
|
"team_race",
|
|
]);
|
|
assert.equal(sessions[2].durationMin, 6);
|
|
assert.equal(sessions[2].seedMethod, "consecutive");
|
|
assert.equal(sessions[2].followUpSec, 20);
|
|
assert.equal(sessions[5].durationMin, 180);
|
|
assert.deepEqual(sessions[5].driverIds, ["driver_1", "driver_2"]);
|
|
});
|
|
|
|
test("qualifying generation splits seeded drivers into every configured round", () => {
|
|
const state = { sessions: [] };
|
|
const event = {
|
|
id: "event_1",
|
|
raceConfig: {
|
|
qualifyingRounds: 2,
|
|
carsPerHeat: 3,
|
|
qualDurationMin: 7,
|
|
qualStartMode: "staggered",
|
|
qualSeedLapCount: 2,
|
|
qualSeedMethod: "average",
|
|
followUpSec: 12,
|
|
},
|
|
};
|
|
const standings = Array.from({ length: 7 }, (_, index) => ({
|
|
driverId: `driver_${index + 1}`,
|
|
rank: index + 1,
|
|
}));
|
|
let cleared = false;
|
|
const created = generateQualifyingForRace(event, {
|
|
buildPracticeStandings: () => standings,
|
|
getEventDrivers: () => [],
|
|
state,
|
|
normalizeStartMode,
|
|
normalizeSession: (session) => session,
|
|
uid: createUid(),
|
|
chunkArray,
|
|
clearGeneratedQualifying: () => { cleared = true; },
|
|
});
|
|
|
|
assert.equal(cleared, true);
|
|
assert.equal(created, 6);
|
|
assert.deepEqual(state.sessions.map((session) => session.name), ["Q1 H1", "Q2 H1", "Q1 H2", "Q2 H2", "Q1 H3", "Q2 H3"]);
|
|
assert.deepEqual(state.sessions[0].driverIds, ["driver_1", "driver_2", "driver_3"]);
|
|
assert.deepEqual(state.sessions[4].driverIds, ["driver_7"]);
|
|
assert.equal(state.sessions[0].durationMin, 7);
|
|
assert.equal(state.sessions[0].seedMethod, "average");
|
|
assert.equal(state.sessions[0].followUpSec, 12);
|
|
});
|
|
|
|
test("qualifying reseed updates ready auto grids but leaves manual grids locked", () => {
|
|
const sessions = [
|
|
{ name: "Q2 H1", type: "qualification", generated: true, status: "ready", driverIds: ["old_1"], manualGridIds: ["old_1"], gridCustomized: false },
|
|
{ name: "Q2 H2", type: "qualification", generated: true, status: "ready", driverIds: ["locked"], manualGridIds: ["locked"], gridCustomized: true },
|
|
{ name: "Q1 H1", type: "qualification", generated: true, status: "finished", driverIds: ["finished"] },
|
|
];
|
|
const standings = ["driver_3", "driver_1", "driver_2"].map((driverId, index) => ({ driverId, rank: index + 1 }));
|
|
const result = reseedUpcomingQualifying(
|
|
{ id: "event_1", raceConfig: { carsPerHeat: 2 } },
|
|
{
|
|
buildQualifyingStandings: () => standings,
|
|
buildPracticeStandings: () => [],
|
|
getEventDrivers: () => [],
|
|
getSessionsForEvent: () => sessions,
|
|
chunkArray,
|
|
}
|
|
);
|
|
|
|
assert.deepEqual(result, { updated: 1, locked: 1, mismatched: 0 });
|
|
assert.deepEqual(sessions[0].driverIds, ["driver_3", "driver_1"]);
|
|
assert.deepEqual(sessions[1].driverIds, ["locked"]);
|
|
assert.deepEqual(sessions[2].driverIds, ["finished"]);
|
|
});
|
|
|
|
test("qualifying reseed sorts heats numerically, not as text (H10 must not sort before H2)", () => {
|
|
const sessions = ["H1", "H2", "H3", "H4", "H5", "H6", "H7", "H8", "H9", "H10", "H11"].map((heat) => ({
|
|
name: `Q1 ${heat}`, type: "qualification", generated: true, status: "ready", driverIds: [], manualGridIds: [], gridCustomized: false,
|
|
}));
|
|
const standings = Array.from({ length: 22 }, (_, index) => ({ driverId: `driver_${index + 1}`, rank: index + 1 }));
|
|
const result = reseedUpcomingQualifying(
|
|
{ id: "event_1", raceConfig: { carsPerHeat: 2 } },
|
|
{ buildQualifyingStandings: () => standings, buildPracticeStandings: () => [], getEventDrivers: () => [], getSessionsForEvent: () => sessions, chunkArray }
|
|
);
|
|
|
|
assert.deepEqual(result, { updated: 11, locked: 0, mismatched: 0 });
|
|
const heat2 = sessions.find((session) => session.name === "Q1 H2");
|
|
const heat10 = sessions.find((session) => session.name === "Q1 H10");
|
|
assert.deepEqual(heat2.driverIds, ["driver_3", "driver_4"], "the 2nd-fastest pair lands in heat 2, not wherever text-sort would put it");
|
|
assert.deepEqual(heat10.driverIds, ["driver_19", "driver_20"], "heat 10 gets the pair its numeric position actually calls for");
|
|
});
|
|
|
|
test("qualifying reseed reports a field-size mismatch instead of silently dropping or ghosting heats", () => {
|
|
const sessions = [
|
|
{ name: "Q1 H1", type: "qualification", generated: true, status: "ready", driverIds: [], manualGridIds: [], gridCustomized: false },
|
|
{ name: "Q1 H2", type: "qualification", generated: true, status: "ready", driverIds: [], manualGridIds: [], gridCustomized: false },
|
|
];
|
|
// 4 heats' worth of drivers now exist, but only 2 ready heat sessions do.
|
|
const standings = Array.from({ length: 8 }, (_, index) => ({ driverId: `driver_${index + 1}`, rank: index + 1 }));
|
|
const result = reseedUpcomingQualifying(
|
|
{ id: "event_1", raceConfig: { carsPerHeat: 2 } },
|
|
{ buildQualifyingStandings: () => standings, buildPracticeStandings: () => [], getEventDrivers: () => [], getSessionsForEvent: () => sessions, chunkArray }
|
|
);
|
|
|
|
assert.equal(result.mismatched, 1, "the round's ready-heat count no longer matches the participant list");
|
|
});
|
|
|
|
test("final generation reserves bump slots and keeps seeded grid order", () => {
|
|
const state = { sessions: [] };
|
|
const sourceRows = Array.from({ length: 7 }, (_, index) => ({ driverId: `driver_${index + 1}` }));
|
|
const event = {
|
|
id: "event_1",
|
|
raceConfig: {
|
|
finalsSource: "qualifying",
|
|
finalLegs: 2,
|
|
carsPerFinal: 4,
|
|
finalDurationMin: 8,
|
|
finalStartMode: "position",
|
|
followUpSec: 15,
|
|
bumpCount: 1,
|
|
reserveBumpSlots: true,
|
|
},
|
|
};
|
|
const created = generateFinalsForRace(event, {
|
|
buildPracticeStandings: () => [],
|
|
buildQualifyingStandings: () => sourceRows,
|
|
clearGeneratedFinals: () => {},
|
|
state,
|
|
normalizeStartMode,
|
|
normalizeSession: (session) => session,
|
|
uid: createUid(),
|
|
chunkArray,
|
|
t: (key) => key,
|
|
});
|
|
|
|
assert.equal(created, 6);
|
|
assert.deepEqual(state.sessions.map((session) => session.name), [
|
|
"A session.final 1",
|
|
"A session.final 2",
|
|
"B session.final 1",
|
|
"B session.final 2",
|
|
"C session.final 1",
|
|
"C session.final 2",
|
|
]);
|
|
assert.deepEqual(state.sessions[0].driverIds, ["driver_1", "driver_2", "driver_3"]);
|
|
assert.equal(state.sessions[0].reservedBumpSlots, 1);
|
|
assert.equal(state.sessions[4].reservedBumpSlots, 0);
|
|
assert.deepEqual(state.sessions[0].manualGridIds, state.sessions[0].driverIds);
|
|
});
|
|
|
|
test("final generation never produces more than 26 mains, even with a large field and an aggressive bump count", () => {
|
|
const state = { sessions: [] };
|
|
const sourceRows = Array.from({ length: 60 }, (_, index) => ({ driverId: `driver_${index + 1}` }));
|
|
const event = {
|
|
id: "event_1",
|
|
raceConfig: {
|
|
finalsSource: "qualifying", finalLegs: 1, carsPerFinal: 4, finalDurationMin: 5,
|
|
finalStartMode: "position", followUpSec: 0, bumpCount: 50, reserveBumpSlots: true,
|
|
},
|
|
};
|
|
generateFinalsForRace(event, {
|
|
buildPracticeStandings: () => [], buildQualifyingStandings: () => sourceRows, clearGeneratedFinals: () => {},
|
|
state, normalizeStartMode, normalizeSession: (session) => session, uid: createUid(), chunkArray, t: (key) => key,
|
|
});
|
|
|
|
assert.ok(state.sessions.length <= 26, `expected at most 26 mains, got ${state.sessions.length}`);
|
|
assert.ok(state.sessions.every((session) => /^[A-Z] /.test(session.name)), "every main name is a single real letter, never running past Z");
|
|
});
|
|
|
|
test("bump-up promotes lower-main winners into reserved upper-main slots and removes them from the lower main's remaining legs", () => {
|
|
const sessions = [
|
|
{ name: "A final 1", type: "final", status: "ready", maxCars: 4, reservedBumpSlots: 1, driverIds: ["a1", "a2", "a3"], manualGridIds: ["a1", "a2", "a3"] },
|
|
// B final 1 already ran (produced the standings below) and keeps its real result. B
|
|
// final 2 is the next leg still to come - that's the one that should lose the
|
|
// promoted driver, not the one that already happened.
|
|
{ name: "B final 1", type: "final", status: "finished", maxCars: 4, driverIds: ["b1", "b2", "b3"] },
|
|
{ name: "B final 2", type: "final", status: "ready", maxCars: 4, reservedBumpSlots: 1, driverIds: ["b1", "b2", "b3"], manualGridIds: ["b1", "b2", "b3"] },
|
|
{ name: "C final 1", type: "final", status: "finished", maxCars: 4, driverIds: ["c1"] },
|
|
];
|
|
const standings = [
|
|
{ rank: "A1", driverId: "a1" },
|
|
{ rank: "B1", driverId: "b1" },
|
|
{ rank: "B2", driverId: "b2" },
|
|
{ rank: "C1", driverId: "c1" },
|
|
];
|
|
const applied = applyBumpsForRace(
|
|
{ id: "event_1", raceConfig: { bumpCount: 1, carsPerFinal: 4, reserveBumpSlots: true } },
|
|
{ buildFinalStandings: () => standings, getSessionsForEvent: () => sessions }
|
|
);
|
|
|
|
assert.equal(applied, 2);
|
|
assert.deepEqual(sessions[0].driverIds, ["a1", "a2", "a3", "b1"], "b1 promoted into A");
|
|
assert.deepEqual(sessions[1].driverIds, ["b1", "b2", "b3"], "B's already-finished leg keeps its real historical result");
|
|
assert.deepEqual(sessions[2].driverIds, ["b2", "b3", "c1"], "b1 removed from B's upcoming leg (promoted out), c1 added (promoted in) - no more double-entry");
|
|
assert.equal(sessions[0].reservedBumpSlots, 0);
|
|
});
|
|
|
|
test("bump-up still grows a main when reserveBumpSlots is off, instead of silently doing nothing because the main looks full", () => {
|
|
const sessions = [
|
|
{ name: "A final 1", type: "final", status: "ready", maxCars: 4, reservedBumpSlots: 0, driverIds: ["a1", "a2", "a3", "a4"], manualGridIds: ["a1", "a2", "a3", "a4"] },
|
|
{ name: "B final 1", type: "final", status: "finished", maxCars: 4, driverIds: ["b1", "b2", "b3", "b4"] },
|
|
];
|
|
const standings = [
|
|
{ rank: "A1", driverId: "a1" },
|
|
{ rank: "B1", driverId: "b1" },
|
|
];
|
|
const applied = applyBumpsForRace(
|
|
{ id: "event_1", raceConfig: { bumpCount: 1, carsPerFinal: 4, reserveBumpSlots: false } },
|
|
{ buildFinalStandings: () => standings, getSessionsForEvent: () => sessions }
|
|
);
|
|
|
|
assert.equal(applied, 1, "b1 is actually added, not blocked because A already has 4/4 seeded drivers");
|
|
assert.deepEqual(sessions[0].driverIds, ["a1", "a2", "a3", "a4", "b1"]);
|
|
});
|
|
|
|
test("session timing handles untimed practice, countdown, and follow-up", () => {
|
|
const getTarget = (session) => getSessionTargetMs(session, { isUntimedSession });
|
|
|
|
assert.equal(getTarget({ type: "open_practice", durationMin: 5 }), null);
|
|
assert.equal(getTarget({ type: "final", durationMin: 5 }), 300000);
|
|
|
|
const timing = getSessionTiming(
|
|
{ type: "final", durationMin: 5, startedAt: 1000, followUpSec: 15, followUpStartedAt: 301000 },
|
|
306000,
|
|
{ getSessionTargetMs: getTarget }
|
|
);
|
|
assert.equal(timing.elapsedMs, 305000);
|
|
assert.equal(timing.remainingMs, 0);
|
|
assert.equal(timing.followUpActive, true);
|
|
assert.equal(timing.followUpRemainingMs, 10000);
|
|
|
|
const untimed = getSessionTiming({ type: "open_practice", startedAt: 1000 }, 61000, { getSessionTargetMs: getTarget });
|
|
assert.equal(untimed.untimed, true);
|
|
assert.equal(untimed.remainingMs, null);
|
|
assert.equal(untimed.elapsedMs, 60000);
|
|
});
|
|
|
|
test("a finished session's elapsed time is frozen at endedAt, not still counting up against the real clock", () => {
|
|
const getTarget = (session) => getSessionTargetMs(session, { isUntimedSession });
|
|
const finished = { type: "final", durationMin: 5, status: "finished", startedAt: 1000, endedAt: 301000 };
|
|
|
|
const rightAfter = getSessionTiming(finished, 301000, { getSessionTargetMs: getTarget });
|
|
const muchLater = getSessionTiming(finished, 301000 + 3600000, { getSessionTargetMs: getTarget });
|
|
assert.equal(rightAfter.elapsedMs, 300000);
|
|
assert.equal(muchLater.elapsedMs, 300000, "checking an hour later must report the exact same, fixed elapsed time");
|
|
});
|
|
|
|
test("getCompetitorElapsedMs excludes the session's accumulated paused time", () => {
|
|
const session = { startedAt: 1000, totalPausedMs: 5000 };
|
|
assert.equal(getCompetitorElapsedMs(session, { lastTimestamp: 21000 }), 15000);
|
|
assert.equal(getCompetitorElapsedMs({ startedAt: 1000, totalPausedMs: 0 }, { lastTimestamp: 21000 }), 20000, "no pause, no change");
|
|
assert.equal(getCompetitorElapsedMs(session, { lastTimestamp: 0 }), 0, "no passing recorded yet");
|
|
});
|
|
|
|
test("buildLeaderboard excludes paused wall-clock time from a competitor's elapsed/comparison time, so a paused heat stays comparable to an unpaused one", () => {
|
|
const result = {
|
|
competitors: {
|
|
driver_1: { key: "driver_1", driverName: "One", startTimestamp: 1000, laps: 1, lastLapMs: 20000, lastTimestamp: 21000, bestLapMs: 20000, manualLapAdjustment: 0, manualTimeAdjustmentMs: 0 },
|
|
},
|
|
};
|
|
const passings = [{ competitorKey: "driver_1", timestamp: 21000, lapMs: 20000, validLap: true }];
|
|
// A 5000ms track call happened mid-heat - nobody actually raced during it, so it must
|
|
// not count against this driver's comparison time.
|
|
const session = { id: "s1", type: "final", startedAt: 1000, totalPausedMs: 5000, durationMin: 5 };
|
|
|
|
const rows = buildLeaderboard(session, {
|
|
ensureSessionResult: () => result,
|
|
getSessionTargetMs: (s) => getSessionTargetMs(s, { isUntimedSession }),
|
|
getCompetitorPassings: (_s, row, options = {}) => passings
|
|
.filter((passing) => passing.competitorKey === row.key)
|
|
.filter((passing) => options.includeInvalid || passing.validLap !== false),
|
|
isCountedPassing: (passing) => passing.validLap !== false,
|
|
getCompetitorElapsedMs,
|
|
getCompetitorSeedMetric: () => null,
|
|
getSessionLapWindow: () => ({ minLapMs: 0, maxLapMs: Infinity }),
|
|
getPassingValidationLabel: () => "",
|
|
formatSeedMetric: () => "",
|
|
formatRaceClock: (ms) => `${ms}`,
|
|
formatLeaderboardGap: () => "",
|
|
formatLapDelta: () => "",
|
|
formatLap: (ms) => `${ms}`,
|
|
t: (key) => key,
|
|
});
|
|
|
|
assert.equal(rows.length, 1);
|
|
assert.equal(rows[0].totalElapsedMs, 15000, "the 5000ms pause is excluded from the driver's own elapsed/comparison time");
|
|
assert.equal(rows[0].comparisonMs, 15000);
|
|
});
|
|
|
|
test("race lap window and seed metrics enforce timing rules", () => {
|
|
const state = {
|
|
events: [
|
|
{ id: "race", mode: "race", raceConfig: { minLapMs: 11000, maxLapMs: 60000 } },
|
|
{ id: "track", mode: "track" },
|
|
{ id: "invalid", mode: "race", raceConfig: { minLapMs: 20000, maxLapMs: 15000 } },
|
|
],
|
|
};
|
|
assert.deepEqual(getSessionLapWindow({ eventId: "race" }, { state }), { minLapMs: 11000, maxLapMs: 60000 });
|
|
assert.deepEqual(getSessionLapWindow({ eventId: "invalid" }, { state }), { minLapMs: 20000, maxLapMs: 20000 });
|
|
assert.equal(getSessionLapWindow({ eventId: "track" }, { state }).maxLapMs, Number.POSITIVE_INFINITY);
|
|
|
|
const passings = [16000, 21000, 16500, 17000].map((lapMs) => ({ lapMs }));
|
|
const consecutive = getCompetitorSeedMetric(
|
|
{ seedBestLapCount: 2, seedMethod: "consecutive" },
|
|
{},
|
|
{ getCompetitorPassings: () => passings }
|
|
);
|
|
const best = getCompetitorSeedMetric(
|
|
{ seedBestLapCount: 2, seedMethod: "best_sum" },
|
|
{},
|
|
{ getCompetitorPassings: () => passings }
|
|
);
|
|
assert.deepEqual(consecutive.laps, [16500, 17000]);
|
|
assert.equal(consecutive.totalMs, 33500);
|
|
assert.deepEqual(best.laps, [16000, 16500]);
|
|
assert.equal(best.totalMs, 32500);
|
|
});
|
|
|
|
test("points tables, tie comparison, and leaderboard gaps remain deterministic", () => {
|
|
assert.equal(getQualifyingPointsValue(2, 8, "rank_low"), 2);
|
|
assert.equal(getQualifyingPointsValue(2, 8, "field_desc"), 7);
|
|
assert.equal(getQualifyingPointsValue(2, 8, "ifmar"), 9);
|
|
assert.equal(isHighPointsTable("rank_low"), false);
|
|
assert.equal(isHighPointsTable("ifmar"), true);
|
|
assert.equal(compareNumberSet([1, 3], [1, 4]), -1);
|
|
assert.equal(compareNumberSet([10, 8], [10, 7], true), -1);
|
|
|
|
const t = (key) => key;
|
|
const leader = { key: "leader", laps: 17, comparisonMs: 16000, totalElapsedMs: 301000, distanceToTargetMs: 1000 };
|
|
assert.equal(formatLeaderboardGap(leader, leader, { isFreePractice: true }, { t }), "status.free_practice");
|
|
assert.equal(formatLeaderboardGap({ key: "second", laps: 17, comparisonMs: 16550 }, leader, { isFreePractice: true }, { t }), "+0.550s");
|
|
assert.equal(formatLeaderboardGap({ key: "second", laps: 16, totalElapsedMs: 290000 }, leader, {}, { t }), "+1L");
|
|
assert.equal(
|
|
formatLeaderboardGap(
|
|
{ key: "second", laps: 17, totalElapsedMs: 306000, distanceToTargetMs: 6000 },
|
|
leader,
|
|
{ useTargetTieBreak: true },
|
|
{ t }
|
|
),
|
|
"+5.000s"
|
|
);
|
|
});
|
|
|
|
test("final standings count the configured best legs per main", () => {
|
|
const sessions = [
|
|
{ id: "a1", type: "final", name: "A final 1" },
|
|
{ id: "a2", type: "final", name: "A final 2" },
|
|
{ id: "a3", type: "final", name: "A final 3" },
|
|
{ id: "b1", type: "final", name: "B final 1" },
|
|
];
|
|
const rowsBySession = {
|
|
a1: [
|
|
{ driverId: "driver_1", driverName: "One", totalElapsedMs: 301000 },
|
|
{ driverId: "driver_2", driverName: "Two", totalElapsedMs: 302000 },
|
|
],
|
|
a2: [
|
|
{ driverId: "driver_2", driverName: "Two", totalElapsedMs: 300000 },
|
|
{ driverId: "driver_1", driverName: "One", totalElapsedMs: 303000 },
|
|
],
|
|
a3: [
|
|
{ driverId: "driver_1", driverName: "One", totalElapsedMs: 299000 },
|
|
{ driverId: "driver_2", driverName: "Two", totalElapsedMs: 304000 },
|
|
],
|
|
b1: [{ driverId: "driver_3", driverName: "Three", totalElapsedMs: 305000 }],
|
|
};
|
|
const standings = buildFinalStandings(
|
|
{ id: "event_1", raceConfig: { countedFinalLegs: 2 } },
|
|
{ getSessionsForEvent: () => sessions, buildLeaderboard: (session) => rowsBySession[session.id] }
|
|
);
|
|
|
|
assert.deepEqual(standings.map((row) => row.rank), ["A1", "A2", "B1"]);
|
|
assert.equal(standings[0].driverId, "driver_1");
|
|
assert.equal(standings[0].score, "2 (1+1+2)");
|
|
assert.equal(standings[1].score, "3 (1+2+2)");
|
|
});
|
|
|
|
test("final standings keep a driver who never recorded a passing (DNS) instead of dropping them, ranked last in their main", () => {
|
|
const sessions = [{ id: "a1", type: "final", name: "A final 1", status: "finished" }];
|
|
const rowsBySession = {
|
|
a1: [{ driverId: "driver_1", driverName: "One", totalElapsedMs: 301000, resultDisplay: "12/5:01.000" }],
|
|
};
|
|
const entrants = [{ id: "driver_1", name: "One" }, { id: "driver_2", name: "Two" }];
|
|
const standings = buildFinalStandings(
|
|
{ id: "event_1", raceConfig: {} },
|
|
{
|
|
getSessionsForEvent: () => sessions,
|
|
buildLeaderboard: (session) => rowsBySession[session.id],
|
|
getSessionEntrants: () => entrants,
|
|
t: testTranslate,
|
|
}
|
|
);
|
|
|
|
assert.deepEqual(standings.map((row) => row.driverId), ["driver_1", "driver_2"]);
|
|
assert.equal(standings[1].driverId, "driver_2");
|
|
assert.equal(standings[1].rank, "A2", "the DNS driver still gets a real, last-place rank in their main");
|
|
|
|
// A not-yet-run final must not invent DNS rows for drivers who simply haven't raced yet.
|
|
const notYetRun = buildFinalStandings(
|
|
{ id: "event_1", raceConfig: {} },
|
|
{
|
|
getSessionsForEvent: () => [{ id: "a1", type: "final", name: "A final 1", status: "ready" }],
|
|
buildLeaderboard: () => rowsBySession.a1,
|
|
getSessionEntrants: () => entrants,
|
|
t: testTranslate,
|
|
}
|
|
);
|
|
assert.deepEqual(notYetRun.map((row) => row.driverId), ["driver_1"], "driver_2 hasn't raced yet, not DNS - correctly absent");
|
|
});
|
|
|
|
test("qualifying standings keep a driver who never recorded a passing (DNS) so they aren't silently dropped from future rounds or finals", () => {
|
|
const sessions = [{ id: "q1h1", type: "qualification", name: "Q1 H1", status: "finished" }];
|
|
const rows = [{ driverId: "driver_1", driverName: "One", comparisonMs: 16000, bestLapMs: 16000, resultDisplay: "16.000" }];
|
|
const entrants = [{ id: "driver_1", name: "One" }, { id: "driver_2", name: "Two" }];
|
|
const standings = buildQualifyingStandings(
|
|
{ id: "event_1", raceConfig: {} },
|
|
{
|
|
getSessionsForEvent: () => sessions,
|
|
buildLeaderboard: () => rows,
|
|
getSessionEntrants: () => entrants,
|
|
isHighPointsTable,
|
|
getQualifyingPointsValue,
|
|
compareNumberSet,
|
|
buildQualifyingTieBreakNote: (row, tieBreak) => buildQualifyingTieBreakNoteLogic(row, tieBreak, { t: testTranslate, formatLap: testFormatLap }),
|
|
formatLap: testFormatLap,
|
|
formatRaceClock: testFormatRaceClock,
|
|
t: testTranslate,
|
|
hasQualifyingPrimaryTie,
|
|
}
|
|
);
|
|
|
|
assert.deepEqual(standings.map((row) => row.driverId), ["driver_1", "driver_2"], "driver_2 (DNS) still appears, ranked last");
|
|
assert.equal(standings[1].bestSingleLapMs, Number.MAX_SAFE_INTEGER, "a DNS driver's missing lap is never confused for a 0ms fastest lap");
|
|
});
|
|
|
|
test("a driver who DNS'd in one heat but raced normally in the others is still fully counted across all of them, not dropped", () => {
|
|
const sessions = [
|
|
{ id: "q1h1", type: "qualification", name: "Q1 H1", status: "finished" },
|
|
{ id: "q2h1", type: "qualification", name: "Q2 H1", status: "finished" },
|
|
{ id: "q3h1", type: "qualification", name: "Q3 H1", status: "finished" },
|
|
];
|
|
// driver_2 DNS'd heat 1 (dead battery), but raced - and won - heats 2 and 3.
|
|
const rowsBySession = {
|
|
q1h1: [{ driverId: "driver_1", driverName: "One", comparisonMs: 16000, bestLapMs: 16000, resultDisplay: "16.000" }],
|
|
q2h1: [
|
|
{ driverId: "driver_2", driverName: "Two", comparisonMs: 15000, bestLapMs: 15000, resultDisplay: "15.000" },
|
|
{ driverId: "driver_1", driverName: "One", comparisonMs: 16500, bestLapMs: 16500, resultDisplay: "16.500" },
|
|
],
|
|
q3h1: [
|
|
{ driverId: "driver_2", driverName: "Two", comparisonMs: 14800, bestLapMs: 14800, resultDisplay: "14.800" },
|
|
{ driverId: "driver_1", driverName: "One", comparisonMs: 16200, bestLapMs: 16200, resultDisplay: "16.200" },
|
|
],
|
|
};
|
|
const entrants = [{ id: "driver_1", name: "One" }, { id: "driver_2", name: "Two" }];
|
|
const standings = buildQualifyingStandings(
|
|
// Counting all 3 rounds (not just the best one) so a dropped round would be visible
|
|
// in the total score, not silently hidden by "best of N" already excluding it.
|
|
{ id: "event_1", raceConfig: { countedQualRounds: 3 } },
|
|
{
|
|
getSessionsForEvent: () => sessions,
|
|
buildLeaderboard: (session) => rowsBySession[session.id],
|
|
getSessionEntrants: () => entrants,
|
|
isHighPointsTable,
|
|
getQualifyingPointsValue,
|
|
compareNumberSet,
|
|
buildQualifyingTieBreakNote: (row, tieBreak) => buildQualifyingTieBreakNoteLogic(row, tieBreak, { t: testTranslate, formatLap: testFormatLap }),
|
|
formatLap: testFormatLap,
|
|
formatRaceClock: testFormatRaceClock,
|
|
t: testTranslate,
|
|
hasQualifyingPrimaryTie,
|
|
}
|
|
);
|
|
|
|
const driverTwo = standings.find((row) => row.driverId === "driver_2");
|
|
assert.ok(driverTwo, "driver_2 still appears at all - not dropped because of the heat 1 DNS");
|
|
assert.deepEqual(driverTwo.ranks.sort((a, b) => a - b), [1, 1, 2], "won heat 2 and heat 3 outright (rank 1), only heat 1's DNS counts against them (rank 2 of 2)");
|
|
assert.equal(driverTwo.countedPoints.length, 3, "all three heats count toward the score, including the DNS one - it isn't excluded or silently skipped");
|
|
});
|
|
|
|
function buildQualifyingStandingsForTest(event, sessions, rowsBySession, entrants) {
|
|
return buildQualifyingStandings(event, {
|
|
getSessionsForEvent: () => sessions,
|
|
buildLeaderboard: (session) => rowsBySession[session.id],
|
|
getSessionEntrants: () => entrants,
|
|
isHighPointsTable,
|
|
getQualifyingPointsValue,
|
|
compareNumberSet,
|
|
buildQualifyingTieBreakNote: (row, tieBreak) => buildQualifyingTieBreakNoteLogic(row, tieBreak, { t: testTranslate, formatLap: testFormatLap }),
|
|
formatLap: testFormatLap,
|
|
formatRaceClock: testFormatRaceClock,
|
|
t: testTranslate,
|
|
hasQualifyingPrimaryTie,
|
|
});
|
|
}
|
|
|
|
test("a DNS driver is scored correctly under a high-points table (field_desc), not just the default low-points one", () => {
|
|
const sessions = [
|
|
{ id: "h1", type: "qualification", name: "Q1 H1", status: "finished" },
|
|
{ id: "h2", type: "qualification", name: "Q2 H1", status: "finished" },
|
|
{ id: "h3", type: "qualification", name: "Q3 H1", status: "finished" },
|
|
];
|
|
const entrants = [{ id: "driver_a", name: "A" }, { id: "driver_b", name: "B" }];
|
|
const rowsBySession = {
|
|
// driver_a DNS'd heat 1 (driver_b is the only real finisher there), then won heats 2
|
|
// and 3 outright with driver_b second both times.
|
|
h1: [{ driverId: "driver_b", driverName: "B", comparisonMs: 16000, resultDisplay: "16.000" }],
|
|
h2: [
|
|
{ driverId: "driver_a", driverName: "A", comparisonMs: 15500, resultDisplay: "15.500" },
|
|
{ driverId: "driver_b", driverName: "B", comparisonMs: 16200, resultDisplay: "16.200" },
|
|
],
|
|
h3: [
|
|
{ driverId: "driver_a", driverName: "A", comparisonMs: 15400, resultDisplay: "15.400" },
|
|
{ driverId: "driver_b", driverName: "B", comparisonMs: 16100, resultDisplay: "16.100" },
|
|
],
|
|
};
|
|
const standings = buildQualifyingStandingsForTest(
|
|
{ id: "event_1", raceConfig: { qualifyingPointsTable: "field_desc", countedQualRounds: 3 } },
|
|
sessions,
|
|
rowsBySession,
|
|
entrants
|
|
);
|
|
|
|
// field_desc in a 2-driver field: 1st = 2 points, 2nd (or DNS, ranked after the field) = 1.
|
|
const driverA = standings.find((row) => row.driverId === "driver_a");
|
|
const driverB = standings.find((row) => row.driverId === "driver_b");
|
|
assert.equal(driverA.totalScore, 1 + 2 + 2, "1 point for the DNS heat (ranked last, not zero and not the winner's points), 2 for each heat won");
|
|
assert.equal(driverB.totalScore, 2 + 1 + 1, "2 points for winning the DNS heat by default, 1 for each 2nd place");
|
|
assert.equal(standings[0].driverId, "driver_a", "two outright wins plus a DNS still beats three consistent 2nd places under a high-points table");
|
|
});
|
|
|
|
test("a DNS driver is scored correctly under \"best time\" qualifying (not points-based) - a bad DNS heat never contaminates their best result", () => {
|
|
const sessions = [
|
|
{ id: "h1", type: "qualification", name: "Q1 H1", status: "finished" },
|
|
{ id: "h2", type: "qualification", name: "Q2 H1", status: "finished" },
|
|
];
|
|
const entrants = [{ id: "driver_a", name: "A" }, { id: "driver_b", name: "B" }];
|
|
const rowsBySession = {
|
|
h1: [{ driverId: "driver_b", driverName: "B", comparisonMs: 20000, resultDisplay: "20.000" }], // driver_a DNS
|
|
h2: [
|
|
{ driverId: "driver_a", driverName: "A", comparisonMs: 15000, resultDisplay: "15.000" }, // driver_a's genuine best time
|
|
{ driverId: "driver_b", driverName: "B", comparisonMs: 16000, resultDisplay: "16.000" },
|
|
],
|
|
};
|
|
const standings = buildQualifyingStandingsForTest(
|
|
{ id: "event_1", raceConfig: { qualifyingScoring: "best" } },
|
|
sessions,
|
|
rowsBySession,
|
|
entrants
|
|
);
|
|
|
|
const driverA = standings.find((row) => row.driverId === "driver_a");
|
|
assert.equal(driverA.bestRank, 1, "driver_a's single best placing (1st in heat 2) is what counts, unaffected by the DNS heat's last place");
|
|
assert.equal(driverA.bestRoundMetric, 15000, "their genuine 15.000s best time, not inflated by the DNS heat's Number.MAX_SAFE_INTEGER placeholder");
|
|
assert.equal(standings[0].driverId, "driver_a", "ranks ahead of driver_b, who never beat a 16.000s best");
|
|
});
|
|
|
|
test("session channel statistics summarize splits and pit stops per competitor", () => {
|
|
const result = {
|
|
competitors: {
|
|
team_1: { displayName: "Team One", driverName: "Driver One", carName: "Car One" },
|
|
},
|
|
splits: [
|
|
{ competitorKey: "team_1", splitMs: 8000 },
|
|
{ competitorKey: "team_1", splitMs: 7600 },
|
|
],
|
|
pitStops: [
|
|
{ competitorKey: "team_1", driverName: "Driver One", carName: "Car One", enteredAt: 100000, exitedAt: 112000, pitStopMs: 12000 },
|
|
{ competitorKey: "team_1", driverName: "Driver Two", carName: "Car Two", enteredAt: 200000, exitedAt: 209000, pitStopMs: 9000 },
|
|
],
|
|
};
|
|
const stats = buildSessionChannelStats({ id: "session_1" }, { ensureSessionResult: () => result });
|
|
assert.equal(stats.splitCount, 2);
|
|
assert.equal(stats.pitStopCount, 2);
|
|
assert.equal(stats.totalPitMs, 21000);
|
|
assert.equal(stats.competitors[0].averageSplitMs, 7800);
|
|
assert.equal(stats.competitors[0].bestPitStopMs, 9000);
|
|
assert.equal(stats.pitStops[1].driverName, "Driver Two");
|
|
});
|
|
|
|
test("paused session timing freezes and resumes without counting the pause", () => {
|
|
const getSessionTargetMs = () => 300_000;
|
|
const paused = getSessionTiming({ startedAt: 1_000, pausedAt: 61_000, totalPausedMs: 0, followUpSec: 0 }, 121_000, { getSessionTargetMs });
|
|
assert.equal(paused.elapsedMs, 60_000);
|
|
assert.equal(paused.remainingMs, 240_000);
|
|
const resumed = getSessionTiming({ startedAt: 1_000, pausedAt: null, totalPausedMs: 60_000, followUpSec: 0 }, 121_000, { getSessionTargetMs });
|
|
assert.equal(resumed.elapsedMs, 60_000);
|
|
assert.equal(resumed.remainingMs, 240_000);
|
|
});
|
|
|
|
test("ensureSessionResult backfills a missing passings array on an already-existing result", () => {
|
|
// A result can already exist (loaded from an older export, or a hand-edited/imported race
|
|
// package) without ever going through the brand-new-session initialization branch, the same
|
|
// way it can be missing adjustments/splits/pitEvents/pitStops - passings needs the same guard.
|
|
const state = { resultsBySession: { session_1: { competitors: {} } } };
|
|
const result = ensureSessionResult("session_1", { state });
|
|
assert.deepEqual(result.passings, []);
|
|
assert.deepEqual(result.adjustments, []);
|
|
assert.deepEqual(result.splits, []);
|
|
assert.deepEqual(result.pitEvents, []);
|
|
assert.deepEqual(result.pitStops, []);
|
|
|
|
const brandNew = ensureSessionResult("session_2", { state });
|
|
assert.deepEqual(brandNew, { passings: [], splits: [], pitEvents: [], pitStops: [], competitors: {}, adjustments: [] });
|
|
});
|