fix(training): keep practice laps in training stats after Reset data
Resetting a running Open Practice deleted its results, the Timing view recreated an empty result at once, and the next save rebuilt that session's training laps from nothing - wiping every earlier lap from the drivers' stats. Reset now syncs first and records resultsResetAt; laps from before it are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
69
tests/browser_practice_reset.js
Normal file
69
tests/browser_practice_reset.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { startFakeDecoder } from "./browser_fake_decoder.js";
|
||||
|
||||
// "Reset data" on Timing for an Open Practice that keeps running (cleared for a new day):
|
||||
// the timing view starts empty, but every lap from before stays in the training stats - also
|
||||
// after the next lap brings the session back into the results, which used to wipe them.
|
||||
export async function runPracticeResetSmoke({ evaluate, navigate, savedState, waitFor, base, dialogs }) {
|
||||
const patchState = async (patch) => {
|
||||
const current = await savedState();
|
||||
Object.assign(current, patch);
|
||||
const response = await fetch(base + "/api/state", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(current) });
|
||||
assert.equal(response.ok, true);
|
||||
};
|
||||
const driverLaps = async () => ((await savedState()).training?.ledger || []).filter((lap) => lap.driverId === "pr_driver").map((lap) => lap.lapMs);
|
||||
|
||||
const dialogsAtStart = dialogs.length;
|
||||
const decoder = await startFakeDecoder();
|
||||
const seeded = await savedState();
|
||||
const event = { id: "pr_event", name: "PR Practice", classId: "pr_class", mode: "race", raceConfig: { practiceOnly: true } };
|
||||
const startedAt = Date.now() - 600000;
|
||||
const practice = { id: "pr_practice", eventId: event.id, name: "PR Open Practice", type: "open_practice", durationMin: 600, status: "running", startedAt };
|
||||
const lap = (lapMs, timestamp) => ({ driverId: "pr_driver", driverName: "PR Driver", competitorKey: "pr_driver", transponder: "8220001", lapMs, timestamp, validLap: true });
|
||||
await patchState({
|
||||
classes: [...seeded.classes, { id: "pr_class", name: "PR Class" }],
|
||||
drivers: [...seeded.drivers, { id: "pr_driver", name: "PR Driver", classId: "pr_class", transponder: "8220001" }],
|
||||
events: [...seeded.events, event],
|
||||
sessions: [...seeded.sessions.map((session) => (["running", "paused"].includes(session.status) ? { ...session, status: "finished", endedAt: Date.now() } : session)), practice],
|
||||
resultsBySession: {
|
||||
...seeded.resultsBySession,
|
||||
[practice.id]: {
|
||||
passings: [lap(20000, startedAt + 20000), lap(19500, startedAt + 39500), lap(19000, startedAt + 58500)],
|
||||
competitors: { pr_driver: { key: "pr_driver", driverId: "pr_driver", driverName: "PR Driver", transponder: "8220001", laps: 3, lastLapMs: 19000, bestLapMs: 19000, startTimestamp: startedAt, lastTimestamp: startedAt + 58500 } },
|
||||
},
|
||||
},
|
||||
activeSessionId: practice.id,
|
||||
settings: { ...seeded.settings, autoReconnect: true, wsUrl: `ws://127.0.0.1:${decoder.port}` },
|
||||
});
|
||||
|
||||
try {
|
||||
await navigate("timing", "resetSession");
|
||||
// The laps were seeded straight into the results and haven't been synced into the training
|
||||
// stats yet - Reset itself must sync them before clearing (the case of laps driven since
|
||||
// the last save).
|
||||
const before = dialogs.length;
|
||||
await evaluate(`document.getElementById("resetSession").click()`);
|
||||
await waitFor(async () => dialogs.slice(before).some((dialog) => dialog.type === "confirm" && /training|tränings/i.test(dialog.message)), "confirm says the laps stay in the training stats");
|
||||
// The Timing view recreates an empty result for the active session straight away - which
|
||||
// is why the old code lost the laps already on the next save, not only on the next lap.
|
||||
await waitFor(async () => ((await savedState()).resultsBySession?.[practice.id]?.passings || []).length === 0, "timing data cleared");
|
||||
assert.deepEqual((await driverLaps()).sort(), [19000, 19500, 20000], "laps kept right after the reset");
|
||||
assert.equal((await savedState()).sessions.find((session) => session.id === practice.id).status, "running", "the practice keeps running");
|
||||
|
||||
await waitFor(async () => evaluate('document.getElementById("connectionBadge")?.classList.contains("badge-online")'), "decoder connected");
|
||||
decoder.sendPassing("8220001");
|
||||
await waitFor(async () => ((await savedState()).resultsBySession?.[practice.id]?.passings || []).length >= 1, "a new lap after the reset");
|
||||
await waitFor(async () => (await driverLaps()).length === 4, "the new lap joins the training stats");
|
||||
assert.deepEqual((await driverLaps()).slice(0, 3), [20000, 19500, 19000], "the laps from before the reset are still there");
|
||||
console.log("PASS resetting a running Open Practice clears timing but keeps every lap in the training stats");
|
||||
} finally {
|
||||
dialogs.splice(dialogsAtStart);
|
||||
const current = await savedState();
|
||||
await patchState({
|
||||
settings: { ...current.settings, autoReconnect: false },
|
||||
sessions: current.sessions.map((session) => (session.id === practice.id ? { ...session, status: "finished", endedAt: Date.now() } : session)),
|
||||
activeSessionId: "",
|
||||
});
|
||||
await decoder.close();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { runAuthSmoke } from "./browser_auth.js";
|
||||
import { runTtsSettingsSmoke } from "./browser_tts_settings.js";
|
||||
import { runTrackLayoutsSmoke } from "./browser_track_layouts.js";
|
||||
import { runPrologueSmoke } from "./browser_prologue.js";
|
||||
import { runPracticeResetSmoke } from "./browser_practice_reset.js";
|
||||
import { runObsPopupSmoke } from "./browser_obs_popup.js";
|
||||
import { runDecoderReconnectSmoke } from "./browser_decoder_reconnect.js";
|
||||
import assert from "node:assert/strict";
|
||||
@@ -152,6 +153,7 @@ try {
|
||||
await runTtsSettingsSmoke({ evaluate, navigate, savedState, waitFor });
|
||||
await runTrackLayoutsSmoke({ evaluate, navigate, savedState, waitFor, base, dialogs });
|
||||
await runPrologueSmoke({ evaluate, navigate, savedState, waitFor, base, dialogs });
|
||||
await runPracticeResetSmoke({ evaluate, navigate, savedState, waitFor, base, dialogs });
|
||||
await runRaceDaySmoke({ evaluate, navigate, fill, savedState, waitFor, pause, base, dialogs });
|
||||
await runRaceDayStartSequencesSmoke({ evaluate, navigate, waitFor, pause, base, dialogs });
|
||||
await runObsPopupSmoke({ evaluate, navigate, fill, savedState, waitFor, pause, base, debugPort, dialogs, realClick });
|
||||
|
||||
@@ -151,6 +151,34 @@ test("ledger sync retains archived laps and rebuilds current session laps", () =
|
||||
assert.equal(synced.markers.length, 1);
|
||||
});
|
||||
|
||||
test("resetting a running Open Practice keeps its earlier laps in the training stats", () => {
|
||||
const lap = (lapMs, timestamp) => ({ driverId: "driver_1", competitorKey: "driver_1", lapMs, timestamp, validLap: true });
|
||||
const state = {
|
||||
sessions: [{ id: "op", name: "Open Practice", type: "open_practice", eventId: "event_1" }],
|
||||
events: [{ id: "event_1", name: "Club" }],
|
||||
resultsBySession: { op: { passings: [lap(20000, 1000), lap(19500, 2000), lap(19000, 3000)] } },
|
||||
};
|
||||
let training = syncTrainingLedger({}, state, { uid: createUid() });
|
||||
assert.equal(training.ledger.length, 3);
|
||||
|
||||
// "Reset data" on Timing: the session keeps running, its results are cleared.
|
||||
state.sessions[0].resultsResetAt = 5000;
|
||||
delete state.resultsBySession.op;
|
||||
training = syncTrainingLedger(training, state, { uid: createUid() });
|
||||
assert.equal(training.ledger.length, 3, "nothing is lost right after the reset");
|
||||
|
||||
// The next day's first lap brings the session back into the results. This used to wipe
|
||||
// every lap from before the reset out of the training stats.
|
||||
state.resultsBySession.op = { passings: [lap(18500, 90000)] };
|
||||
training = syncTrainingLedger(training, state, { uid: createUid() });
|
||||
assert.deepEqual(training.ledger.map((entry) => entry.lapMs), [20000, 19500, 19000, 18500]);
|
||||
|
||||
// A lap removed from the current results (judge invalidated it) still leaves the stats.
|
||||
state.resultsBySession.op.passings[0].validLap = false;
|
||||
training = syncTrainingLedger(training, state, { uid: createUid() });
|
||||
assert.deepEqual(training.ledger.map((entry) => entry.lapMs), [20000, 19500, 19000]);
|
||||
});
|
||||
|
||||
test("driver statistics remain isolated and aggregate days and sessions", () => {
|
||||
const firstDay = new Date(2026, 0, 10, 12, 0, 0).getTime();
|
||||
const secondDay = new Date(2026, 0, 11, 12, 0, 0).getTime();
|
||||
|
||||
Reference in New Issue
Block a user