feat(training): base training stats on practice, keep race laps separate
Training stats count Open/Free practice by default; laps from qualifying, heats and finals are kept and shown under Based on: Races or All, marked with race, session and series. Races are listed per race session and never split into stints. Same picker in the mobile profile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
buildTrainingPaceBuckets,
|
||||
buildTrainingRecommendation,
|
||||
buildTrainingStintAnalysis,
|
||||
buildTrainingRaceSessions,
|
||||
buildTrainingWeekdayStats,
|
||||
countExerciseSessions,
|
||||
countDriverExerciseSessions,
|
||||
@@ -983,3 +984,47 @@ test("layout choice \"none\" picks the laps driven before any layout existed", (
|
||||
assert.deepEqual(getDriverTrainingLapsForLayout(state, "driver_1", "", "current").map((lap) => lap.lapMs), [16_000]);
|
||||
assert.equal(getDriverTrainingLapsForLayout(state, "driver_1", "", "all").length, 2);
|
||||
});
|
||||
|
||||
test("training stats count practice by default; race laps are kept, marked and shown per race session, never as stints", () => {
|
||||
let n = 0;
|
||||
const uid = (prefix) => `${prefix}_${++n}`;
|
||||
const lap = (sessionId, timestamp, lapMs) => ({ driverId: "d1", competitorKey: "d1", sessionId, lapMs, timestamp, validLap: true });
|
||||
const state = {
|
||||
drivers: [{ id: "d1", name: "D1", classId: "c1" }],
|
||||
classes: [{ id: "c1", name: "2WD" }],
|
||||
events: [{ id: "e_practice", name: "Club practice" }, { id: "e_race", name: "Winter #3 2WD" }],
|
||||
sessions: [
|
||||
{ id: "op", name: "Open Practice", type: "open_practice", eventId: "e_practice" },
|
||||
{ id: "q2", name: "Kval 2", type: "qualification", eventId: "e_race" },
|
||||
],
|
||||
series: [{ id: "s", name: "Winter Cup", eventIds: ["e_race"] }],
|
||||
resultsBySession: {
|
||||
op: { passings: [lap("op", 1000, 20000), lap("op", 21000, 20000), lap("op", 41000, 19500), lap("op", 60500, 19500)] },
|
||||
q2: { passings: [lap("q2", 500000, 18000), lap("q2", 518000, 18200), lap("q2", 536200, 18400)] },
|
||||
},
|
||||
};
|
||||
state.training = syncTrainingLedger({}, state, { uid });
|
||||
assert.deepEqual(state.training.ledger.map((entry) => entry.sessionType), ["open_practice", "open_practice", "open_practice", "open_practice", "qualification", "qualification", "qualification"]);
|
||||
|
||||
assert.equal(getDriverTrainingLaps(state, "d1").length, 4, "practice by default");
|
||||
assert.equal(getDriverTrainingLaps(state, "d1", "", { source: "race" }).length, 3);
|
||||
assert.equal(getDriverTrainingLaps(state, "d1", "", { source: "all" }).length, 7);
|
||||
assert.equal(buildDriverTrainingStats(state, "d1").bestLapMs, 19500, "a race lap doesn't become the training best");
|
||||
assert.equal(buildDriverTrainingStats(state, "d1", "", { source: "all" }).bestLapMs, 18000);
|
||||
|
||||
// Stints come from practice laps only, even with "all"; with "race" there are none.
|
||||
const allStints = buildTrainingStintAnalysis(state, "d1", "", { source: "all" });
|
||||
assert.equal(allStints.rows.reduce((sum, row) => sum + row.lapCount, 0), 4);
|
||||
assert.equal(buildTrainingStintAnalysis(state, "d1", "", { source: "race" }).rows.length, 0);
|
||||
|
||||
const races = buildTrainingRaceSessions(state, "d1");
|
||||
assert.equal(races.length, 1);
|
||||
assert.deepEqual([races[0].sessionName, races[0].eventName, races[0].eventId, races[0].lapCount, races[0].bestLapMs, races[0].averageLapMs], ["Kval 2", "Winter #3 2WD", "e_race", 3, 18000, 18200]);
|
||||
|
||||
// A lap saved before session types were recorded gets it from its (still existing) session.
|
||||
const old = { ...state, training: { ...state.training, ledger: state.training.ledger.map(({ sessionType, ...entry }) => entry) } };
|
||||
assert.equal(getDriverTrainingLaps(old, "d1", "", { source: "race" }).length, 3);
|
||||
delete old.resultsBySession;
|
||||
const resynced = syncTrainingLedger(old.training, old, { uid });
|
||||
assert.equal(resynced.ledger.filter((entry) => entry.sessionType === "qualification").length, 3, "back-filled on the next sync");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user