import assert from "node:assert/strict"; // Regression coverage for the race day Schedule panel (Event Manager's "Schedule" tab). // buildEventSchedule's cascading-time math and the getSessionSortWeight ordering bug it // exposed are already covered at the unit level (race_timing_logic.test.js) - this file // exercises the UI wiring on top of it: the form actually persists scheduleStartAt/ // scheduleGapMin onto event.raceConfig (previously silently dropped by normalizeEvent's // field allowlist), the table renders sessions in running order (not seed order), a // session that started late shows the Delayed badge, and Clear removes only the start // time, not the configured gap. export async function runEventScheduleSmoke({ evaluate, navigate, fill, savedState, waitFor, base }) { const click = (id) => evaluate(`document.getElementById(${JSON.stringify(id)}).click()`); const submit = (id) => evaluate(`document.getElementById(${JSON.stringify(id)}).requestSubmit()`); const findEvent = async (id) => (await savedState()).events.find((event) => event.id === id); 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 classId = "esc_class"; const driverA = { id: "esc_driver_a", name: "ESC Driver A", classId, transponder: "7830001" }; const raceEvent = { id: "esc_event", name: "ESC Event", classId, mode: "race", raceConfig: { participantsConfigured: true, driverIds: [driverA.id], teams: [] }, }; // practice (sort weight 2) runs before qualification (weight 3) - buildEventSchedule // orders by getSessionSortWeight, not seed/array order, so the table proves that too. const qualifyingSession = { id: "esc_quali", eventId: raceEvent.id, name: "ESC Qualification", type: "qualification", durationMin: 5, status: "ready" }; const practiceSession = { id: "esc_practice", eventId: raceEvent.id, name: "ESC Practice", type: "practice", durationMin: 5, status: "ready" }; const seeded = await savedState(); await patchState({ drivers: [...seeded.drivers, driverA], events: [...seeded.events, raceEvent], sessions: [...seeded.sessions, qualifyingSession, practiceSession], }); const openSchedule = async () => { await navigate("race_setup", "raceWizardStepForm"); await click("event-manage-" + raceEvent.id); await waitFor(async () => await evaluate(`Boolean(document.querySelector(".manage-tabs"))`), "manage tabs rendered"); await evaluate(`document.querySelector('[data-manage-tab="schedule"]').click()`); await waitFor(async () => await evaluate(`Boolean(document.getElementById("scheduleForm"))`), "schedule panel visible"); }; await openSchedule(); assert.equal(await evaluate(`document.querySelector('[data-manage-panel="schedule"]')?.hidden`), false, "schedule panel is visible after clicking its tab"); assert.equal(await evaluate(`Boolean(document.querySelector('[data-manage-panel="schedule"] table'))`), false, "with no start time set yet, the not-set hint is shown instead of a table"); await fill("scheduleForm", { scheduleStartAt: "2026-09-11T10:00", scheduleGapMin: "5" }); await submit("scheduleForm"); await waitFor(async () => Number.isFinite((await findEvent(raceEvent.id)).raceConfig.scheduleStartAt), "schedule form saves scheduleStartAt onto the event"); const savedEvent = await findEvent(raceEvent.id); assert.equal(savedEvent.raceConfig.scheduleGapMin, 5, "schedule form saves scheduleGapMin onto the event"); const startAt = savedEvent.raceConfig.scheduleStartAt; console.log("PASS saving the schedule form persists scheduleStartAt/scheduleGapMin onto event.raceConfig"); await openSchedule(); const rowNames = await evaluate(`Array.from(document.querySelectorAll('[data-manage-panel="schedule"] table tbody tr td:first-child')).map((td) => td.textContent)`); assert.deepEqual(rowNames, [practiceSession.name, qualifyingSession.name], "the schedule table lists sessions in running order (practice before qualification), not seed order"); const delayedBadgesBefore = await evaluate(`document.querySelectorAll('[data-manage-panel="schedule"] .pill-warn').length`); assert.equal(delayedBadgesBefore, 0, "no session has started yet, so nothing is flagged delayed"); console.log("PASS the schedule table cascades planned times in running order with no delayed sessions yet"); // Start practice 2.5 minutes after its own planned time - past buildEventSchedule's // 1-minute delayed threshold - and leave it running (no endedAt) so the table has to // show a real actualStart with no actualEnd. await patchState({ sessions: (await savedState()).sessions.map((session) => session.id === practiceSession.id ? { ...session, status: "running", startedAt: startAt + 150000 } : session ), }); await openSchedule(); const actualCells = await evaluate(`Array.from(document.querySelectorAll('[data-manage-panel="schedule"] table tbody tr td:nth-child(4)')).map((td) => td.textContent)`); assert.notEqual(actualCells[0], "-", "practice's actual start is shown once the session has started"); const delayedBadgesAfter = await evaluate(`document.querySelectorAll('[data-manage-panel="schedule"] .pill-warn').length`); assert.equal(delayedBadgesAfter, 1, "the late-started practice session is flagged delayed"); console.log("PASS a session that started more than a minute late is flagged with the Delayed badge"); await evaluate(`document.getElementById("scheduleClear").click()`); await waitFor(async () => (await findEvent(raceEvent.id)).raceConfig.scheduleStartAt === null, "Clear removes scheduleStartAt"); assert.equal((await findEvent(raceEvent.id)).raceConfig.scheduleGapMin, 5, "Clear leaves the configured gap untouched"); await openSchedule(); assert.equal(await evaluate(`Boolean(document.querySelector('[data-manage-panel="schedule"] table'))`), false, "with no start time set, the schedule table is replaced by the not-set hint"); console.log("PASS clearing the schedule removes the start time but keeps the configured gap"); }