Each layout can carry its own lap window (set on creation, editable in Settings, saved from Race Day during practice). Open/Free practice on that layout uses it; races always keep the event's values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
7.6 KiB
JavaScript
100 lines
7.6 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");
|
|
|
|
// Settings: the layout gets its own min/max lap time; max <= min is refused.
|
|
await navigate("settings", "settingsForm");
|
|
const layoutId = (await savedState()).trackLayouts[0].id;
|
|
const submitLapWindow = (min, max) => evaluate(`(() => { const form = document.querySelector('[data-layout-lap-window="${layoutId}"]'); form.elements.minLapSec.value = "${min}"; form.elements.maxLapSec.value = "${max}"; form.requestSubmit(); })()`);
|
|
await submitLapWindow("8.5", "40");
|
|
await waitFor(async () => (await savedState()).trackLayouts[0].minLapMs === 8500, "layout min lap time saved");
|
|
assert.equal((await savedState()).trackLayouts[0].maxLapMs, 40000);
|
|
const alertsBefore = dialogs.length;
|
|
await submitLapWindow("30", "20");
|
|
await waitFor(async () => dialogs.slice(alertsBefore).some((dialog) => dialog.type === "alert"), "an impossible lap window is refused");
|
|
assert.equal((await savedState()).trackLayouts[0].minLapMs, 8500, "unchanged");
|
|
console.log("PASS each track layout can have its own min/max lap time");
|
|
|
|
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");
|
|
|
|
// A running open practice moves onto the new layout and its best laps start over, so the
|
|
// old layout's times can't block or fake a personal best on the rebuilt track.
|
|
await patchState({
|
|
sessions: (await savedState()).sessions.map((session) => (session.id === practice.id ? { ...session, status: "running", startedAt: Date.now() - 60000 } : session)),
|
|
resultsBySession: { ...(await savedState()).resultsBySession, [practice.id]: { passings: [], competitors: { tl_driver: { key: "tl_driver", driverName: "TL Driver", laps: 3, bestLapMs: 18000, bestLapLayoutId: (await savedState()).trackLayouts[0].id } } } },
|
|
});
|
|
await navigate("training", "view");
|
|
await evaluate(`document.querySelector("[data-track-layout-new]").click()`);
|
|
await waitFor(async () => (await savedState()).trackLayouts?.length === 2, "second layout created");
|
|
const afterSwitch = await savedState();
|
|
const newLayoutId = afterSwitch.trackLayouts[1].id;
|
|
assert.equal(afterSwitch.sessions.find((session) => session.id === practice.id).trackLayoutId, newLayoutId, "the running practice moved onto the new layout");
|
|
const competitor = afterSwitch.resultsBySession[practice.id].competitors.tl_driver;
|
|
assert.equal(competitor.bestLapMs, null, "the old layout's best lap no longer counts");
|
|
assert.equal(competitor.bestLapLayoutId, newLayoutId);
|
|
assert.equal(competitor.laps, 3, "laps already driven are kept");
|
|
await patchState({ sessions: (await savedState()).sessions.map((session) => (session.id === practice.id ? { ...session, status: "finished", endedAt: Date.now() } : session)) });
|
|
console.log("PASS a new layout during open practice restarts every driver's best lap");
|
|
|
|
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, 2, "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)),
|
|
});
|
|
}
|
|
}
|