Add race timing and judging regression tests
This commit is contained in:
@@ -2073,6 +2073,23 @@ npm test
|
|||||||
- personal timer aggregate and frozen-window calculations
|
- personal timer aggregate and frozen-window calculations
|
||||||
- clock-skew-safe query normalization and bounds
|
- clock-skew-safe query normalization and bounds
|
||||||
|
|
||||||
|
`tests/race_timing_logic.test.js` covers:
|
||||||
|
- race-format parsing, defaults, lap limits, participants, and teams
|
||||||
|
- wizard generation of practice, qualifying, and team-race sessions
|
||||||
|
- qualifying heat generation and manual-grid-safe reseeding
|
||||||
|
- final generation, reserved bump slots, and bump-up promotion
|
||||||
|
- timed, follow-up, and untimed session clocks
|
||||||
|
- race lap windows and best/consecutive seed metrics
|
||||||
|
- qualifying points, tie comparisons, and leaderboard gaps
|
||||||
|
- multi-leg final standings and counted-leg scoring
|
||||||
|
|
||||||
|
`tests/judging_logic.test.js` covers:
|
||||||
|
- cumulative lap/time corrections, reset, and correction-state undo
|
||||||
|
- manual lap invalidation and competitor-result recalculation
|
||||||
|
- restoration and undo of passing validity
|
||||||
|
- safe handling when no counted lap exists
|
||||||
|
- competitor and judging-log filters
|
||||||
|
|
||||||
The suite uses Node's built-in `node:test` runner and does not add a test-framework dependency. Windows PowerShell/CMD can run the same suite with `npm.cmd test`.
|
The suite uses Node's built-in `node:test` runner and does not add a test-framework dependency. Windows PowerShell/CMD can run the same suite with `npm.cmd test`.
|
||||||
|
|
||||||
## 19. Recommended Future Additions
|
## 19. Recommended Future Additions
|
||||||
@@ -2082,4 +2099,4 @@ If the project grows further, the next useful additions to this document are:
|
|||||||
- exact `settings` schema snapshot
|
- exact `settings` schema snapshot
|
||||||
- SQLite table contract section
|
- SQLite table contract section
|
||||||
- controller-to-view ID map for critical forms/buttons
|
- controller-to-view ID map for critical forms/buttons
|
||||||
- additional test checklists for race generation, timing, judging, and persistence modules
|
- additional test checklists for persistence modules
|
||||||
|
|||||||
163
tests/judging_logic.test.js
Normal file
163
tests/judging_logic.test.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import {
|
||||||
|
applyCompetitorCorrection,
|
||||||
|
findPassingByUndoMarker,
|
||||||
|
getJudgeFilteredLog,
|
||||||
|
getJudgeFilteredRows,
|
||||||
|
getManualCorrectionSummary,
|
||||||
|
invalidateCompetitorLastLap,
|
||||||
|
recalculateCompetitorFromPassings,
|
||||||
|
restoreCompetitorLastInvalidLap,
|
||||||
|
undoJudgingAdjustment,
|
||||||
|
} from "../src/judging_logic.js";
|
||||||
|
|
||||||
|
function createHarness() {
|
||||||
|
const session = { id: "session_1", startedAt: 1000 };
|
||||||
|
const result = {
|
||||||
|
passings: [
|
||||||
|
{ competitorKey: "driver_1", timestamp: 17000, lapMs: 16000, validLap: true },
|
||||||
|
{ competitorKey: "driver_1", timestamp: 34000, lapMs: 17000, validLap: true },
|
||||||
|
{ competitorKey: "driver_1", timestamp: 50500, lapMs: 16500, validLap: true },
|
||||||
|
],
|
||||||
|
competitors: {
|
||||||
|
driver_1: {
|
||||||
|
key: "driver_1",
|
||||||
|
driverName: "Driver One",
|
||||||
|
startTimestamp: 1000,
|
||||||
|
laps: 3,
|
||||||
|
lastLapMs: 16500,
|
||||||
|
lastTimestamp: 50500,
|
||||||
|
bestLapMs: 16000,
|
||||||
|
manualLapAdjustment: 0,
|
||||||
|
manualTimeAdjustmentMs: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adjustments: [],
|
||||||
|
};
|
||||||
|
let id = 0;
|
||||||
|
let saveCount = 0;
|
||||||
|
const ensureSessionResult = () => result;
|
||||||
|
const isCountedPassing = (passing) => passing.validLap !== false;
|
||||||
|
const getCompetitorPassings = (_session, row, options = {}) => result.passings
|
||||||
|
.filter((passing) => passing.competitorKey === row.key)
|
||||||
|
.filter((passing) => options.includeInvalid || isCountedPassing(passing))
|
||||||
|
.sort((left, right) => left.timestamp - right.timestamp);
|
||||||
|
const recalculate = (targetSession, rowKey) => recalculateCompetitorFromPassings(targetSession, rowKey, {
|
||||||
|
ensureSessionResult,
|
||||||
|
getCompetitorPassings,
|
||||||
|
isCountedPassing,
|
||||||
|
});
|
||||||
|
const findPassing = (targetSession, rowKey, timestamp) => findPassingByUndoMarker(targetSession, rowKey, timestamp, {
|
||||||
|
ensureSessionResult,
|
||||||
|
});
|
||||||
|
const deps = {
|
||||||
|
ensureSessionResult,
|
||||||
|
getCompetitorPassings,
|
||||||
|
isCountedPassing,
|
||||||
|
recalculateCompetitorFromPassings: recalculate,
|
||||||
|
findPassingByUndoMarker: findPassing,
|
||||||
|
uid: (prefix) => `${prefix}_${++id}`,
|
||||||
|
t: (key) => key,
|
||||||
|
formatLap: (lapMs) => `${lapMs}ms`,
|
||||||
|
saveState: () => { saveCount += 1; },
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
session,
|
||||||
|
result,
|
||||||
|
row: result.competitors.driver_1,
|
||||||
|
deps,
|
||||||
|
getSaveCount: () => saveCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("manual corrections accumulate, reset, and undo to the prior state", () => {
|
||||||
|
const harness = createHarness();
|
||||||
|
|
||||||
|
applyCompetitorCorrection(
|
||||||
|
harness.session,
|
||||||
|
harness.row,
|
||||||
|
{ lapDelta: -1, timeMsDelta: 5000 },
|
||||||
|
harness.deps
|
||||||
|
);
|
||||||
|
assert.equal(harness.row.manualLapAdjustment, -1);
|
||||||
|
assert.equal(harness.row.manualTimeAdjustmentMs, 5000);
|
||||||
|
assert.equal(getManualCorrectionSummary(harness.row, harness.deps), "-1L • +5000ms");
|
||||||
|
assert.equal(harness.result.adjustments[0].detail, "-1L • +5000ms");
|
||||||
|
|
||||||
|
applyCompetitorCorrection(harness.session, harness.row, { reset: true }, harness.deps);
|
||||||
|
assert.equal(harness.row.manualLapAdjustment, 0);
|
||||||
|
assert.equal(harness.row.manualTimeAdjustmentMs, 0);
|
||||||
|
|
||||||
|
const resetAdjustment = harness.result.adjustments[1];
|
||||||
|
assert.equal(undoJudgingAdjustment(harness.session, resetAdjustment.id, harness.deps), true);
|
||||||
|
assert.equal(harness.row.manualLapAdjustment, -1);
|
||||||
|
assert.equal(harness.row.manualTimeAdjustmentMs, 5000);
|
||||||
|
assert.equal(resetAdjustment.undoneAt > 0, true);
|
||||||
|
assert.equal(harness.result.adjustments.at(-1).category, "undo");
|
||||||
|
assert.equal(harness.getSaveCount(), 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalidating and restoring the latest lap recalculates the competitor", () => {
|
||||||
|
const harness = createHarness();
|
||||||
|
|
||||||
|
assert.equal(invalidateCompetitorLastLap(harness.session, harness.row, harness.deps), true);
|
||||||
|
assert.equal(harness.result.passings[2].validLap, false);
|
||||||
|
assert.equal(harness.result.passings[2].invalidReason, "manual_invalid");
|
||||||
|
assert.equal(harness.row.laps, 2);
|
||||||
|
assert.equal(harness.row.lastLapMs, 17000);
|
||||||
|
assert.equal(harness.row.lastTimestamp, 34000);
|
||||||
|
assert.equal(harness.row.bestLapMs, 16000);
|
||||||
|
|
||||||
|
assert.equal(restoreCompetitorLastInvalidLap(harness.session, harness.row, harness.deps), true);
|
||||||
|
assert.equal(harness.result.passings[2].validLap, true);
|
||||||
|
assert.equal(harness.row.laps, 3);
|
||||||
|
assert.equal(harness.row.lastLapMs, 16500);
|
||||||
|
assert.equal(harness.row.lastTimestamp, 50500);
|
||||||
|
assert.equal(harness.getSaveCount(), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("undo restores passing validity and cannot be applied twice", () => {
|
||||||
|
const harness = createHarness();
|
||||||
|
invalidateCompetitorLastLap(harness.session, harness.row, harness.deps);
|
||||||
|
const invalidation = harness.result.adjustments[0];
|
||||||
|
|
||||||
|
assert.equal(undoJudgingAdjustment(harness.session, invalidation.id, harness.deps), true);
|
||||||
|
assert.equal(harness.result.passings[2].validLap, true);
|
||||||
|
assert.equal(harness.result.passings[2].invalidReason, "");
|
||||||
|
assert.equal(harness.row.laps, 3);
|
||||||
|
assert.equal(undoJudgingAdjustment(harness.session, invalidation.id, harness.deps), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("lap invalidation safely rejects competitors without counted laps", () => {
|
||||||
|
const harness = createHarness();
|
||||||
|
harness.result.passings.forEach((passing) => {
|
||||||
|
passing.validLap = false;
|
||||||
|
});
|
||||||
|
assert.equal(invalidateCompetitorLastLap(harness.session, harness.row, harness.deps), false);
|
||||||
|
assert.equal(harness.result.adjustments.length, 0);
|
||||||
|
assert.equal(harness.getSaveCount(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("judging row and action-log filters select only matching entries", () => {
|
||||||
|
const rows = [
|
||||||
|
{ key: "invalid", invalidPending: true },
|
||||||
|
{ key: "corrected", manualLapAdjustment: -1 },
|
||||||
|
{ key: "team", teamId: "team_1" },
|
||||||
|
{ key: "plain" },
|
||||||
|
];
|
||||||
|
const log = [
|
||||||
|
{ id: "correction", category: "correction" },
|
||||||
|
{ id: "invalid", category: "invalid" },
|
||||||
|
{ id: "undo", category: "undo" },
|
||||||
|
];
|
||||||
|
|
||||||
|
assert.deepEqual(getJudgeFilteredRows(rows, "invalid").map((row) => row.key), ["invalid"]);
|
||||||
|
assert.deepEqual(getJudgeFilteredRows(rows, "corrected").map((row) => row.key), ["corrected"]);
|
||||||
|
assert.deepEqual(getJudgeFilteredRows(rows, "team").map((row) => row.key), ["team"]);
|
||||||
|
assert.equal(getJudgeFilteredRows(rows, "all").length, 4);
|
||||||
|
assert.deepEqual(getJudgeFilteredLog(log, "corrections").map((entry) => entry.id), ["correction"]);
|
||||||
|
assert.deepEqual(getJudgeFilteredLog(log, "invalid").map((entry) => entry.id), ["invalid"]);
|
||||||
|
assert.deepEqual(getJudgeFilteredLog(log, "undo").map((entry) => entry.id), ["undo"]);
|
||||||
|
});
|
||||||
383
tests/race_timing_logic.test.js
Normal file
383
tests/race_timing_logic.test.js
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import {
|
||||||
|
applyBumpsForRace,
|
||||||
|
buildRaceFormatConfigFromForm,
|
||||||
|
buildRaceSessionsFromWizard,
|
||||||
|
generateFinalsForRace,
|
||||||
|
generateQualifyingForRace,
|
||||||
|
reseedUpcomingQualifying,
|
||||||
|
} from "../src/event_race_logic.js";
|
||||||
|
import {
|
||||||
|
buildFinalStandings,
|
||||||
|
compareNumberSet,
|
||||||
|
formatLeaderboardGap,
|
||||||
|
getCompetitorSeedMetric,
|
||||||
|
getQualifyingPointsValue,
|
||||||
|
getSessionLapWindow,
|
||||||
|
getSessionTargetMs,
|
||||||
|
getSessionTiming,
|
||||||
|
isHighPointsTable,
|
||||||
|
isUntimedSession,
|
||||||
|
} from "../src/timing_logic.js";
|
||||||
|
|
||||||
|
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("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 });
|
||||||
|
assert.deepEqual(sessions[0].driverIds, ["driver_3", "driver_1"]);
|
||||||
|
assert.deepEqual(sessions[1].driverIds, ["locked"]);
|
||||||
|
assert.deepEqual(sessions[2].driverIds, ["finished"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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("bump-up promotes lower-main winners into reserved upper-main slots", () => {
|
||||||
|
const sessions = [
|
||||||
|
{ name: "A final 1", type: "final", status: "ready", maxCars: 4, reservedBumpSlots: 1, driverIds: ["a1", "a2", "a3"], manualGridIds: ["a1", "a2", "a3"] },
|
||||||
|
{ name: "B final 1", type: "final", status: "ready", maxCars: 4, reservedBumpSlots: 1, driverIds: ["b1", "b2", "b3"], manualGridIds: ["b1", "b2", "b3"] },
|
||||||
|
{ name: "C final 1", type: "final", status: "ready", maxCars: 4, reservedBumpSlots: 0, 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 } },
|
||||||
|
{ buildFinalStandings: () => standings, getSessionsForEvent: () => sessions }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(applied, 2);
|
||||||
|
assert.deepEqual(sessions[0].driverIds, ["a1", "a2", "a3", "b1"]);
|
||||||
|
assert.deepEqual(sessions[1].driverIds, ["b1", "b2", "b3", "c1"]);
|
||||||
|
assert.equal(sessions[0].reservedBumpSlots, 0);
|
||||||
|
assert.equal(sessions[1].reservedBumpSlots, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
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("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)");
|
||||||
|
});
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
import "./training_logic.test.js";
|
import "./training_logic.test.js";
|
||||||
import "./public_training_profile.test.js";
|
import "./public_training_profile.test.js";
|
||||||
|
import "./race_timing_logic.test.js";
|
||||||
|
import "./judging_logic.test.js";
|
||||||
|
|||||||
Reference in New Issue
Block a user