Files
Live_RC/tests/track_layouts_logic.test.js
larssand 627179b95a feat(layouts): switch track layout mid-practice and view stats per layout
A club can run one Open Practice all season and re-lay the track now and then.
Previously a session's layout was frozen at start and every training lap inherited
it, so a season-long practice stayed on its first layout forever, and changing layout
was hidden under Settings -> Appearance.

- Each practice lap now counts toward the layout active when it was driven; a running
  practice session moves onto a new layout when one is added
- "New layout" strip on Training stats, Race Day and the Dashboard, offered only around
  practice; a running race session blocks layout changes everywhere
- "Stats for" selector (current / older layout / before layouts / all layouts) on
  Training stats and in the driver's mobile profile (?layout= on the public API)
- A layout that has practice laps on it can no longer be deleted
- Fix: the buildDriverTrainingStats wrapper in app.js dropped extra arguments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:24:44 +02:00

116 lines
5.9 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import { normalizeTrackLayout, normalizeTrackLayouts, getTrackLayoutIdAt, getTrackLayoutLabel, nextTrackLayoutLabel, isPracticeSessionType, findRaceSessionBlockingLayoutChange, isTrackLayoutInUse } from "../src/track_layouts_logic.js";
import { getTrackRecordMs } from "../src/timing_logic.js";
test("normalizeTrackLayout fills in an id via uid() when missing and coerces startedAt", () => {
const withoutId = normalizeTrackLayout({ label: "Layout 1", startedAt: 1000 }, { uid: (prefix) => `${prefix}_generated` });
assert.equal(withoutId.id, "layout_generated");
assert.equal(withoutId.startedAt, 1000);
const malformed = normalizeTrackLayout({ id: "x", label: " Layout 2 ", startedAt: "not-a-number" });
assert.equal(malformed.label, "Layout 2");
assert.ok(Number.isFinite(malformed.startedAt), "falls back to Date.now() rather than NaN");
});
test("normalizeTrackLayouts sorts chronologically and drops entries with no id", () => {
const layouts = normalizeTrackLayouts([
{ id: "b", label: "Later", startedAt: 2000 },
{ id: "a", label: "Earlier", startedAt: 1000 },
{ label: "" },
], { uid: () => "" });
assert.deepEqual(layouts.map((layout) => layout.id), ["a", "b"]);
});
test("getTrackLayoutIdAt returns the latest layout that has already started, treating layouts as contiguous", () => {
const layouts = [
{ id: "layout_1", label: "Layout 1", startedAt: 1000 },
{ id: "layout_2", label: "Layout 2", startedAt: 2000 },
{ id: "layout_3", label: "Layout 3", startedAt: 3000 },
];
assert.equal(getTrackLayoutIdAt(layouts, 500), "", "before any layout existed");
assert.equal(getTrackLayoutIdAt(layouts, 1000), "layout_1", "exactly at a layout's start");
assert.equal(getTrackLayoutIdAt(layouts, 2500), "layout_2", "between two layouts belongs to the earlier one");
assert.equal(getTrackLayoutIdAt(layouts, 9999), "layout_3", "after the last change, still the latest layout");
});
test("getTrackLayoutIdAt and getTrackLayoutLabel are inert with no layouts configured", () => {
assert.equal(getTrackLayoutIdAt([], Date.now()), "");
assert.equal(getTrackLayoutLabel([], ""), "");
assert.equal(getTrackLayoutLabel([{ id: "layout_1", label: "Layout 1" }], ""), "", "an empty id never matches a real layout");
});
test("getTrackLayoutLabel resolves a known id and nextTrackLayoutLabel numbers sequentially", () => {
const layouts = [{ id: "layout_1", label: "Layout 1", startedAt: 1000 }];
assert.equal(getTrackLayoutLabel(layouts, "layout_1"), "Layout 1");
assert.equal(getTrackLayoutLabel(layouts, "missing"), "");
assert.equal(nextTrackLayoutLabel([]), "Layout 1");
assert.equal(nextTrackLayoutLabel(layouts), "Layout 2");
});
function createTrackRecordState({ sessions, results }) {
return {
events: [{ id: "event_1", classId: "class_1" }],
sessions,
resultsBySession: results,
};
}
test("getTrackRecordMs finds the fastest lap among other sessions in the same class when no layout is set", () => {
const state = createTrackRecordState({
sessions: [
{ id: "session_1", eventId: "event_1" },
{ id: "session_2", eventId: "event_1" },
],
results: {
session_2: { competitors: { a: { bestLapMs: 12345 } } },
},
});
assert.equal(getTrackRecordMs(state.sessions[0], { state }), 12345);
});
test("getTrackRecordMs only compares sessions run on the same track layout once layouts are in use", () => {
const state = createTrackRecordState({
sessions: [
{ id: "current", eventId: "event_1", trackLayoutId: "layout_2" },
{ id: "same_layout", eventId: "event_1", trackLayoutId: "layout_2" },
{ id: "old_layout", eventId: "event_1", trackLayoutId: "layout_1" },
],
results: {
same_layout: { competitors: { a: { bestLapMs: 15000 } } },
old_layout: { competitors: { a: { bestLapMs: 9000 } } },
},
});
// The 9000ms lap belongs to a different, earlier layout - it must not count as the record
// for a lap set on the current layout, even though it is numerically faster.
assert.equal(getTrackRecordMs(state.sessions[0], { state }), 15000);
});
test("getTrackRecordMs does not conflate a layout-tagged session with an untagged legacy one", () => {
const state = createTrackRecordState({
sessions: [
{ id: "current", eventId: "event_1", trackLayoutId: "layout_1" },
{ id: "legacy", eventId: "event_1" },
],
results: {
legacy: { competitors: { a: { bestLapMs: 5000 } } },
},
});
assert.equal(getTrackRecordMs(state.sessions[0], { state }), null, "the legacy session (no trackLayoutId) is a different bucket from layout_1");
});
test("layouts change only around practice: a running or paused race session blocks it", () => {
assert.equal(isPracticeSessionType("open_practice"), true);
assert.equal(isPracticeSessionType("free_practice"), true);
assert.equal(isPracticeSessionType("qualification"), false);
assert.equal(findRaceSessionBlockingLayoutChange([{ type: "open_practice", status: "running" }, { type: "final", status: "ready" }]), null, "running practice and a not-yet-started final don't block");
assert.equal(findRaceSessionBlockingLayoutChange([{ name: "Q1", type: "qualification", status: "paused" }])?.name, "Q1");
assert.equal(findRaceSessionBlockingLayoutChange([{ name: "Heat", type: "heat", status: "running" }])?.name, "Heat");
});
test("a layout is in use when a session started on it or a practice lap was driven on it", () => {
assert.equal(isTrackLayoutInUse("layout_2", { sessions: [{ trackLayoutId: "layout_1" }], ledger: [] }), false);
assert.equal(isTrackLayoutInUse("layout_2", { sessions: [{ trackLayoutId: "layout_2" }] }), true);
assert.equal(isTrackLayoutInUse("layout_2", { sessions: [{ trackLayoutId: "layout_1" }], ledger: [{ trackLayoutId: "layout_2" }] }), true, "a mid-session switch leaves laps on layout_2 even though no session started on it");
});