The user asked specifically to confirm this: a driver who DNS'd in one qualifying heat but raced normally in others must still be fully counted across all of them, not dropped. Traced through the fix (buildQualifyingStandings backfills a DNS row per finished heat, accumulating into the same per-driver entry across every heat they're in) and confirmed it already handles this correctly - added a test that exercises exactly this scenario (DNS in heat 1, wins heat 2 and 3) to lock it in, since this specific multi-heat aggregation path had no prior coverage. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
752 lines
33 KiB
JavaScript
752 lines
33 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");
|
|
});
|
|
|
|
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: [] });
|
|
});
|