Files
Live_RC/tests/race_timing_logic.test.js
larssand 1565baab2a Add staggered-start car call-outs and a per-driver done indicator
Staggered-start qualifying heats now announce each car's transponder
number in grid order (~3s apart) right after the official start,
matching what a track announcer does when releasing cars one at a
time; mass/position starts are unaffected. The Timing leaderboard and
every overlay variant now also show a green "Done" flag next to a
driver's name once their own personal timing window (which already
started at their own first crossing) has elapsed, even while the
session is still running for other drivers.

Also confirmed two things already worked correctly: finals already
always start everyone together with the shared countdown/lights/tone
sequence, and practice's time-up announcement already says "finish
the lap you are on" - only a "2 minutes remaining" cue is genuinely
missing from the existing 1min/30s/10s ladder, left as an open
question for the user.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 21:31:01 +02:00

927 lines
42 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import {
applyBumpsForRace,
buildOpenPracticeBundle,
buildRaceFormatConfigFromForm,
buildRaceSessionsFromWizard,
generateFinalsForRace,
generateQualifyingForRace,
getTeamDriverPool,
moveDriversIntoQualifyingHeat,
normalizeEvent,
parseQualifyingHeatName,
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("parseQualifyingHeatName extracts the round and heat number, or null for anything else", () => {
assert.deepEqual(parseQualifyingHeatName("Q2 H3"), { round: 2, heat: 3 });
assert.deepEqual(parseQualifyingHeatName("Q10 H11"), { round: 10, heat: 11 });
assert.equal(parseQualifyingHeatName("A Final 1"), null);
assert.equal(parseQualifyingHeatName(""), null);
});
test("moveDriversIntoQualifyingHeat moves a lone driver out of their old heat into a new one, in the same round only", () => {
const sessions = [
{ id: "q1h1", name: "Q1 H1", type: "qualification", driverIds: ["d1", "d2"], manualGridIds: ["d1", "d2"] },
{ id: "q1h2", name: "Q1 H2", type: "qualification", driverIds: ["d3"], manualGridIds: ["d3"] },
// A different round's H1 must never be touched by an edit to Q1's heats.
{ id: "q2h1", name: "Q2 H1", type: "qualification", driverIds: ["d1", "d2"], manualGridIds: ["d1", "d2"] },
];
moveDriversIntoQualifyingHeat(sessions, "q1h1", ["d1", "d2", "d3"]);
const q1h1 = sessions.find((s) => s.id === "q1h1");
const q1h2 = sessions.find((s) => s.id === "q1h2");
const q2h1 = sessions.find((s) => s.id === "q2h1");
assert.deepEqual(q1h2.driverIds, [], "d3 was pulled out of Q1 H2, the heat they were lonely in");
assert.deepEqual(q2h1.driverIds, ["d1", "d2"], "Q2's heats are untouched - only Q1's heats are affected by editing a Q1 heat");
// moveDriversIntoQualifyingHeat only clears the OTHER sessions - the caller (the real
// sessionEditForm handler) is the one that sets the target session's own driverIds.
assert.equal(q1h1.id, "q1h1");
});
test("moveDriversIntoQualifyingHeat does nothing for a session with no parseable heat name", () => {
const sessions = [{ id: "s1", name: "Open Practice", type: "practice", driverIds: ["d1"] }];
moveDriversIntoQualifyingHeat(sessions, "s1", ["d1"]);
assert.deepEqual(sessions[0].driverIds, ["d1"], "left untouched - not a qualifying heat, nothing to move");
});
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);
});
// Regression coverage for the user's report: in a staggered-start qualifying heat, each
// driver's own personal 5-minute window runs from their own first crossing, independent of
// the shared session clock - so one driver can be "done" while the session is still
// running for others. Nothing surfaced that distinction anywhere in the UI before this.
test("buildLeaderboard flags a staggered-mode competitor whose own personal window has elapsed, and only in staggered mode", () => {
const buildRows = (session, competitors) => buildLeaderboard(session, {
ensureSessionResult: () => ({ competitors }),
getSessionTargetMs: (s) => getSessionTargetMs(s, { isUntimedSession }),
getCompetitorPassings: () => [],
isCountedPassing: () => true,
getCompetitorElapsedMs,
getCompetitorSeedMetric: () => null,
getSessionLapWindow: () => ({ minLapMs: 0, maxLapMs: Infinity }),
getPassingValidationLabel: () => "",
formatSeedMetric: () => "",
formatRaceClock: (ms) => `${ms}`,
formatLeaderboardGap: () => "",
formatLapDelta: () => "",
formatLap: (ms) => `${ms}`,
t: (key) => key,
});
const now = Date.now();
const competitors = {
driver_done: { key: "driver_done", driverName: "Done", startTimestamp: now - 6 * 60 * 1000, laps: 10, lastLapMs: 17000, lastTimestamp: now - 60000, bestLapMs: 16500, manualLapAdjustment: 0, manualTimeAdjustmentMs: 0 },
driver_still_going: { key: "driver_still_going", driverName: "Still going", startTimestamp: now - 2 * 60 * 1000, laps: 4, lastLapMs: 17000, lastTimestamp: now - 5000, bestLapMs: 16800, manualLapAdjustment: 0, manualTimeAdjustmentMs: 0 },
};
const staggered = buildRows({ id: "q1h1", type: "qualification", startMode: "staggered", durationMin: 5, totalPausedMs: 0 }, competitors);
assert.equal(staggered.find((row) => row.key === "driver_done").personalTimeUp, true, "6 minutes into their own 5-minute window - done");
assert.equal(staggered.find((row) => row.key === "driver_still_going").personalTimeUp, false, "only 2 minutes into their own window - still going");
const mass = buildRows({ id: "final", type: "final", startMode: "mass", durationMin: 5, totalPausedMs: 0 }, competitors);
assert.ok(mass.every((row) => row.personalTimeUp === false), "mass/position starts share one clock already shown at the session level - never flagged per-row");
});
test("buildLeaderboard's personalTimeUp excludes paused time from a staggered competitor's own window too", () => {
const now = Date.now();
const competitors = {
driver_1: { key: "driver_1", driverName: "One", startTimestamp: now - 5 * 60 * 1000 - 30000, laps: 10, lastLapMs: 17000, lastTimestamp: now - 60000, bestLapMs: 16500, manualLapAdjustment: 0, manualTimeAdjustmentMs: 0 },
};
const deps = {
ensureSessionResult: () => ({ competitors }),
getSessionTargetMs: (s) => getSessionTargetMs(s, { isUntimedSession }),
getCompetitorPassings: () => [],
isCountedPassing: () => true,
getCompetitorElapsedMs,
getCompetitorSeedMetric: () => null,
getSessionLapWindow: () => ({ minLapMs: 0, maxLapMs: Infinity }),
getPassingValidationLabel: () => "",
formatSeedMetric: () => "",
formatRaceClock: (ms) => `${ms}`,
formatLeaderboardGap: () => "",
formatLapDelta: () => "",
formatLap: (ms) => `${ms}`,
t: (key) => key,
};
// 5:30 has passed since this driver's own first crossing, but a 1-minute track call is
// excluded from their personal window the same way it already is from the shared clock -
// net real driving time is only 4:30 of their 5:00, so they are not done yet.
const paused = buildLeaderboard({ id: "q1h1", type: "qualification", startMode: "staggered", durationMin: 5, totalPausedMs: 60000 }, deps);
assert.equal(paused[0].personalTimeUp, false);
});
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: [] });
});