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"); // 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)), }); } }