feat(race-day): start heats with an EFRA/IFMAR count-down and flying start
Race Day Start now runs a count-down (Go, 30 s - 5 min, default per event) with calls every minute, 30 s and 10 s, grid call and 9..3 count for grid starts, hold/resume/+1 min/to 30 s/to 10 s/Go now, and starts the heat by itself when it runs out. The transponder check runs during the count-down, replacing the separate warm-up. Adds a flying start mode (horn + clock is running, own clock per car) as the qualifying default, and the latest personal clock start rules from EFRA 9.3 / IFMAR 4.2.3 for flying and staggered starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,6 @@ const SILENCED_EVENTS = Object.fromEntries(RACE_AUDIO_EVENT_DEFINITIONS.map((def
|
||||
export async function runRaceDayStartSequencesSmoke({ evaluate, navigate, waitFor, pause, base, dialogs = [] }) {
|
||||
const click = (id) => evaluate(`document.getElementById(${JSON.stringify(id)}).click()`);
|
||||
const buttonDisabled = async (id) => await evaluate(`document.getElementById(${JSON.stringify(id)})?.disabled`);
|
||||
const buttonText = async (id) => await evaluate(`document.getElementById(${JSON.stringify(id)})?.textContent`);
|
||||
const savedState = async () => (await (await fetch(base + "/api/state")).json()).state;
|
||||
const patchState = async (patch) => {
|
||||
const current = await savedState();
|
||||
@@ -82,23 +81,29 @@ export async function runRaceDayStartSequencesSmoke({ evaluate, navigate, waitFo
|
||||
},
|
||||
});
|
||||
|
||||
// --- Heat: full-length countdown, proving the countdown display itself ticks down
|
||||
// (not just that the session eventually starts) for a competitive session type. ---
|
||||
// --- Heat: a real count-down on Race Day (the prologue), proving the clock ticks down
|
||||
// and the heat only goes running once the 10..3 count and the lights are done. The
|
||||
// global start count-down (startCountdownSeconds) is replaced by the prologue here. ---
|
||||
await patchState({ activeSessionId: sessions[0].id, settings: { ...(await savedState()).settings, raceAudio: { ...(await savedState()).settings.raceAudio, startCountdownSeconds: 10 } } });
|
||||
await navigate("race_day", "raceDayStart");
|
||||
await waitFor(async () => (await buttonDisabled("raceDayStart")) === false, "heat preflight ready (class-based participants accepted)");
|
||||
await evaluate(`document.getElementById("raceDayPrologueLength").value = "30"`);
|
||||
await click("raceDayStart");
|
||||
await waitFor(async () => Boolean(await evaluate(`document.querySelector('[data-prologue="to10"]')`)), "count-down started");
|
||||
await evaluate(`document.querySelector('[data-prologue="to10"]').click()`);
|
||||
assert.equal((await findSession(sessions[0].id)).status, "ready", "heat does not jump straight to running like a practice session");
|
||||
const observedCountdown = new Set();
|
||||
const countdownDeadline = Date.now() + 4000;
|
||||
while (Date.now() < countdownDeadline) {
|
||||
observedCountdown.add(await buttonText("raceDayStart"));
|
||||
observedCountdown.add(await evaluate(`document.querySelector(".race-day-control-panel [data-prologue-clock]")?.textContent || null`));
|
||||
await pause(300);
|
||||
}
|
||||
assert.ok([...observedCountdown].some((text) => /^\d+$/.test(String(text || ""))), "start button showed a ticking countdown digit: " + JSON.stringify([...observedCountdown]));
|
||||
await waitFor(async () => (await findSession(sessions[0].id)).status === "running", "heat running after its full countdown+light sequence", 20000);
|
||||
assert.deepEqual(await recentAudioEventKeys(2), ["start_sequence", "drivers_ready"], "heat's sequence announced drivers_ready then start_sequence, no qualifying_start");
|
||||
console.log("PASS heat start runs a real countdown+light sequence instead of starting instantly");
|
||||
assert.ok([...observedCountdown].filter((text) => /^0:(0\d|10)$/.test(String(text || ""))).length >= 3, "count-down clock ticked down from 10 s: " + JSON.stringify([...observedCountdown]));
|
||||
await waitFor(async () => (await findSession(sessions[0].id)).status === "running", "heat running after its count-down + light sequence", 20000);
|
||||
const heatLog = (await savedState()).audioEventLog.slice(0, 12).map((entry) => entry.event);
|
||||
assert.ok(heatLog.includes("prologue_count") && heatLog.includes("race_10_seconds"), "the count-down called 10 s and 9..3: " + JSON.stringify(heatLog));
|
||||
assert.ok(!heatLog.includes("drivers_ready") && !heatLog.includes("race_30_seconds"), "no second count-down after the prologue: " + JSON.stringify(heatLog));
|
||||
console.log("PASS heat start runs a real count-down + light sequence instead of starting instantly");
|
||||
|
||||
await click("raceDayPause");
|
||||
await waitFor(async () => (await findSession(sessions[0].id)).status === "paused", "heat paused");
|
||||
@@ -121,6 +126,8 @@ export async function runRaceDayStartSequencesSmoke({ evaluate, navigate, waitFo
|
||||
await patchState({ activeSessionId: session.id });
|
||||
await navigate("race_day", "raceDayStart");
|
||||
await waitFor(async () => (await buttonDisabled("raceDayStart")) === false, `${label} preflight ready`);
|
||||
// "Go - start now": no count-down, but the start procedure itself still runs.
|
||||
await evaluate(`(() => { const select = document.getElementById("raceDayPrologueLength"); if (select) select.value = "0"; })()`);
|
||||
await click("raceDayStart");
|
||||
await waitFor(async () => (await findSession(session.id)).status === "running", `${label} running after its sequence`, 10000);
|
||||
assert.deepEqual(await recentAudioEventKeys(expectedRecentKeys.length), expectedRecentKeys, `${label} sequence announced the right events`);
|
||||
|
||||
Reference in New Issue
Block a user