import assert from "node:assert/strict"; import { startFakeDecoder } from "./browser_fake_decoder.js"; // Open Practice: a driver who usually laps around 20 s suddenly does one lap of 14 s (cut // across the track). A real decoder passing must flag it: the "suspiciously short laps" panel // on Timing, a ⚠ next to the driver in the leaderboard and the overlay, not used as the best // lap - and one click on Remove takes it away. export async function runSuspectLapsSmoke({ 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 results = async () => (await savedState()).resultsBySession?.sl_practice; const dialogsAtStart = dialogs.length; const decoder = await startFakeDecoder(); const seeded = await savedState(); const event = { id: "sl_event", name: "SL Practice", classId: "sl_class", mode: "race", raceConfig: { practiceOnly: true, minLapMs: 3000, maxLapMs: 60000 } }; // Six normal laps around 20 s; the decoder then reports the next crossing 14 s later. const lastTs = Date.now() - 1000; const laps = [20400, 19800, 20200, 20600, 20100, 20300]; let ts = lastTs - laps.reduce((sum, value) => sum + value, 0); const startedAt = ts; const passings = laps.map((lapMs) => { ts += lapMs; return { driverId: "sl_driver", driverName: "SL Kid", displayName: "SL Kid", competitorKey: "driver_sl_driver", transponder: "8330001", lapMs, timestamp: ts, validLap: true }; }); const practice = { id: "sl_practice", eventId: event.id, name: "SL Open Practice", type: "open_practice", durationMin: 600, status: "running", startedAt }; await patchState({ classes: [...seeded.classes, { id: "sl_class", name: "SL Class" }], drivers: [...seeded.drivers, { id: "sl_driver", name: "SL Kid", classId: "sl_class", transponder: "8330001" }], 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, competitors: { driver_sl_driver: { key: "driver_sl_driver", driverId: "sl_driver", driverName: "SL Kid", displayName: "SL Kid", transponder: "8330001", laps: 6, lastLapMs: 20300, bestLapMs: 19800, startTimestamp: startedAt, lastTimestamp: lastTs } } }, }, activeSessionId: practice.id, settings: { ...seeded.settings, autoReconnect: true, wsUrl: `ws://127.0.0.1:${decoder.port}` }, }); try { await navigate("timing", "resetSession"); await waitFor(async () => evaluate('document.getElementById("connectionBadge")?.classList.contains("badge-online")'), "decoder connected"); decoder.sendPassing("8330001", lastTs + 14000); await waitFor(async () => (await results())?.passings?.length === 7, "the short lap arrived"); const shortLap = (await results()).passings.at(-1); assert.equal(shortLap.lapMs, 14000); assert.equal(shortLap.suspect, true, "flagged as suspect"); assert.equal((await results()).competitors.driver_sl_driver.bestLapMs, 19800, "not used as the best lap"); await waitFor(async () => Boolean(await evaluate(`document.getElementById("suspectLapsPanel")`)), "suspect panel on Timing"); assert.match(await evaluate(`document.getElementById("suspectLapsPanel").innerText`), /SL Kid/); assert.equal(await evaluate(`Boolean([...document.querySelectorAll(".table-primary")].find((node) => node.textContent.includes("SL Kid"))?.querySelector(".suspect-flag"))`), true, "⚠ in the leaderboard"); console.log("PASS a lap far quicker than the driver's own pace is flagged on Timing and not used as the best lap"); await navigate("overlay", "view"); await waitFor(async () => evaluate(`document.body.innerText.includes("SL Kid")`), "overlay shows the driver"); assert.equal(await evaluate(`[...document.querySelectorAll(".suspect-flag")].length > 0`), true, "⚠ in the overlay"); console.log("PASS the overlay marks the driver with a suspect lap"); await navigate("race_day", "raceDayStart"); await waitFor(async () => Boolean(await evaluate(`document.querySelector('#suspectLapsPanel [data-lap-action="approve"]')`)), "the same panel on Race Day"); console.log("PASS Race Day shows the suspect lap with Approve / Remove"); await navigate("timing", "resetSession"); await waitFor(async () => Boolean(await evaluate(`document.querySelector('#suspectLapsPanel [data-lap-action="remove"]')`)), "Remove button"); await evaluate(`document.querySelector('#suspectLapsPanel [data-lap-action="remove"]').click()`); await waitFor(async () => (await results())?.passings?.at(-1)?.validLap === false, "the short lap removed"); const after = await results(); assert.equal(after.passings.at(-1).invalidReason, "manual_invalid"); assert.equal(after.competitors.driver_sl_driver.laps, 6, "no longer counted"); assert.ok(after.adjustments?.some((adjustment) => adjustment.category === "invalid"), "logged as a judge correction (undoable)"); await waitFor(async () => !(await evaluate(`Boolean(document.getElementById("suspectLapsPanel"))`)), "panel gone once nothing is pending"); console.log("PASS one click removes the suspect lap and it is logged as an undoable correction"); } 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(); } }