Files
Live_RC/tests/browser_track_layouts.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

68 lines
4.8 KiB
JavaScript

import assert from "node:assert/strict";
// Track layouts from the operator's side: create one from Training stats with the
// "New layout" button, see the per-layout stats selector appear, and confirm layout
// changes stay a practice-only action - hidden on Race Day while a race session is in
// focus, and refused outright while a race session is running.
export async function runTrackLayoutsSmoke({ 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 layoutButtonVisible = () => evaluate(`Boolean(document.querySelector("[data-track-layout-new]"))`);
// This scenario triggers prompts/alerts on purpose; drop them afterwards so the shared
// "no unexpected native dialog" checks in later scenarios stay meaningful.
const dialogsAtStart = dialogs.length;
const seeded = await savedState();
const practiceEvent = { id: "tl_practice_event", name: "TL Practice", classId: "test_class", mode: "race", raceConfig: { practiceOnly: true } };
const raceEvent = { id: "tl_race_event", name: "TL Race", classId: "test_class", mode: "race", raceConfig: { minLapMs: 3000, maxLapMs: 60000 } };
const practice = { id: "tl_practice", eventId: practiceEvent.id, name: "TL Open Practice", type: "open_practice", durationMin: 60, status: "ready" };
const quali = { id: "tl_quali", eventId: raceEvent.id, name: "TL Q1", type: "qualification", durationMin: 5, status: "ready" };
// Earlier scenarios in the shared suite can leave a race-format session running, which
// would (correctly) block every layout change - finish those first.
const settled = seeded.sessions.map((session) => (["running", "paused"].includes(session.status) ? { ...session, status: "finished", endedAt: Date.now() } : session));
await patchState({ trackLayouts: [], events: [...seeded.events, practiceEvent, raceEvent], sessions: [...settled, practice, quali] });
try {
await navigate("training", "view");
await waitFor(async () => layoutButtonVisible(), "New layout button on Training stats");
assert.equal(await evaluate(`Boolean(document.getElementById("training-layout"))`), false, "no per-layout selector before any layout exists");
await evaluate(`document.querySelector("[data-track-layout-new]").click()`);
await waitFor(async () => (await savedState()).trackLayouts?.length === 1, "layout created from Training stats");
assert.equal((await savedState()).trackLayouts[0].label, "Layout 1", "an empty prompt answer gets the next default name");
await waitFor(async () => Boolean(await evaluate(`document.getElementById("training-layout")`)), "per-layout selector appears");
const options = await evaluate(`[...document.querySelectorAll("#training-layout option")].map((option) => option.value)`);
assert.deepEqual(options.filter((value) => ["current", "all"].includes(value)), ["current", "all"]);
assert.ok(options.includes((await savedState()).trackLayouts[0].id));
console.log("PASS a track layout can be created from Training stats and the per-layout stats selector appears");
await patchState({ activeSessionId: quali.id });
await navigate("race_day", "raceDayStart");
assert.equal(await layoutButtonVisible(), false, "no New layout button while a race session is in focus");
await patchState({ activeSessionId: practice.id });
await navigate("race_day", "raceDayStart");
assert.equal(await layoutButtonVisible(), true, "New layout is offered when Open Practice is in focus");
console.log("PASS Race Day only offers a new layout when a practice session is in focus");
await patchState({ sessions: (await savedState()).sessions.map((session) => (session.id === quali.id ? { ...session, status: "running", startedAt: Date.now() } : session)) });
await navigate("training", "view");
const dialogCount = dialogs.length;
await evaluate(`document.querySelector("[data-track-layout-new]").click()`);
await waitFor(async () => dialogs.slice(dialogCount).some((dialog) => dialog.type === "alert" && dialog.message.includes("TL Q1")), "blocked with a message naming the running race");
assert.equal((await savedState()).trackLayouts.length, 1, "no layout was added while the race ran");
console.log("PASS a new track layout is refused while a race session is running");
} finally {
dialogs.splice(dialogsAtStart);
await patchState({
trackLayouts: [],
activeSessionId: "",
sessions: (await savedState()).sessions.filter((session) => ![practice.id, quali.id].includes(session.id)),
events: (await savedState()).events.filter((event) => ![practiceEvent.id, raceEvent.id].includes(event.id)),
});
}
}