Files
Live_RC/tests/browser_race_day.js
2026-09-09 07:52:07 +02:00

113 lines
7.2 KiB
JavaScript

import assert from "node:assert/strict";
import { startFakeDecoder } from "./browser_fake_decoder.js";
export async function runRaceDaySmoke({ evaluate, navigate, fill, savedState, waitFor, pause, base, dialogs = [] }) {
const click = id => evaluate(`document.getElementById(${JSON.stringify(id)}).click()`);
const submit = id => evaluate(`document.getElementById(${JSON.stringify(id)}).requestSubmit()`);
const findEvent = async name => (await savedState()).events.find(event => event.name === name);
const findSession = async name => (await savedState()).sessions.find(session => session.name === name);
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 buttonDisabled = async id => await evaluate(`document.getElementById(${JSON.stringify(id)})?.disabled`);
const decoder = await startFakeDecoder();
try {
// Reliability tests must not depend on real AMMC hardware; a socket that only completes
// the WebSocket handshake is enough to satisfy Race Day's decoder-connected preflight check.
const seeded = await savedState();
await patchState({ settings: { ...seeded.settings, autoReconnect: true, wsUrl: `ws://127.0.0.1:${decoder.port}` } });
await navigate("events", "eventForm");
// Decoder connection state is deliberately excluded from buildPersistableState() in
// src/app.js (it's per-tab, not shared/persisted), so check the live connection badge
// in the page instead of polling backend state.
await waitFor(async () => await evaluate('document.getElementById("connectionBadge")?.classList.contains("badge-online")'), "fake decoder connected");
await fill("eventForm", { name: "Race Day Event", date: "2026-09-08", classId: "test_class" });
await submit("eventForm");
await waitFor(async () => Boolean(await findEvent("Race Day Event")), "create race day event");
const event = await findEvent("Race Day Event");
await click("event-manage-" + event.id);
await fill("sessionForm", { name: "Race Day Heat 1", type: "open_practice", durationMin: "5" });
await submit("sessionForm");
await waitFor(async () => Boolean(await findSession("Race Day Heat 1")), "create heat 1");
await fill("sessionForm", { name: "Race Day Heat 2", type: "open_practice", durationMin: "5" });
await submit("sessionForm");
await waitFor(async () => Boolean(await findSession("Race Day Heat 2")), "create heat 2");
const heat1 = await findSession("Race Day Heat 1");
const heat2 = await findSession("Race Day Heat 2");
// Pin the active session explicitly: other scenarios may have left earlier "ready" sessions
// that would otherwise win the Race Day queue's global fallback lookup.
await patchState({ activeSessionId: heat1.id });
await navigate("race_day", "raceDayStart");
await waitFor(async () => (await buttonDisabled("raceDayStart")) === false, "start enabled once preflight is ready");
assert.equal(await buttonDisabled("raceDayPause"), true);
assert.equal(await buttonDisabled("raceDayFinish"), true);
await click("raceDayStart");
await waitFor(async () => (await findSession("Race Day Heat 1")).status === "running", "heat 1 running");
const started = await findSession("Race Day Heat 1");
assert.ok(started.startedAt, "startedAt recorded");
// normalizeSession() in src/app.js stores "not paused" as 0, not null (Number(null) === 0
// passes its Number.isFinite check) - every real usage treats pausedAt as falsy, so match that.
assert.ok(!started.pausedAt, "not paused after start");
await waitFor(async () => (await buttonDisabled("raceDayPause")) === false, "pause enabled after start");
assert.equal(await buttonDisabled("raceDayFinish"), false);
assert.equal(await buttonDisabled("raceDayResume"), true);
await click("raceDayPause");
await waitFor(async () => (await findSession("Race Day Heat 1")).status === "paused", "heat 1 paused");
assert.ok((await findSession("Race Day Heat 1")).pausedAt, "pausedAt recorded");
await waitFor(async () => (await buttonDisabled("raceDayResume")) === false, "resume enabled after pause");
assert.equal(await buttonDisabled("raceDayPause"), true);
await pause(1200);
await click("raceDayResume");
await waitFor(async () => (await findSession("Race Day Heat 1")).status === "running", "heat 1 resumed");
const resumed = await findSession("Race Day Heat 1");
assert.ok(!resumed.pausedAt, "not paused after resume");
assert.equal(resumed.startedAt, started.startedAt, "resume does not reset the original start time");
assert.ok(resumed.totalPausedMs >= 1000, "paused duration accumulated in totalPausedMs");
await waitFor(async () => (await buttonDisabled("raceDayPause")) === false, "pause enabled after resume");
await click("raceDayFinish");
await waitFor(async () => (await findSession("Race Day Heat 1")).status === "finished", "heat 1 finished");
const finished = await findSession("Race Day Heat 1");
assert.ok(finished.endedAt, "endedAt recorded");
assert.ok(!finished.pausedAt, "not paused after finish");
console.log("PASS race day heat 1 start/pause/resume/finish preserves session timing fields");
await waitFor(async () => await evaluate(`Boolean(document.querySelector('[data-race-day-select="${heat2.id}"]'))`), "heat 2 listed as next in queue");
await evaluate(`document.querySelector('[data-race-day-select="${heat2.id}"]').click()`);
await waitFor(async () => (await savedState()).activeSessionId === heat2.id, "heat 2 selected as active session");
await waitFor(async () => (await evaluate('document.querySelector(".race-day-hero h2")?.textContent')) === "Race Day Heat 2", "heat 2 shown as current");
assert.equal(await buttonDisabled("raceDayStart"), false, "next heat is startable");
await click("raceDayStart");
await waitFor(async () => (await findSession("Race Day Heat 2")).status === "running", "heat 2 running");
await click("raceDayFinish");
await waitFor(async () => (await findSession("Race Day Heat 2")).status === "finished", "heat 2 finished");
const heat1After = await findSession("Race Day Heat 1");
assert.equal(heat1After.status, "finished");
assert.equal(heat1After.startedAt, started.startedAt, "heat 1 timing untouched by heat 2's lifecycle");
assert.ok(heat1After.totalPausedMs >= 1000);
console.log("PASS switching to next heat keeps the previous heat's recorded timing intact");
// A native alert()/confirm() anywhere in this flow (e.g. a start validation surprise)
// would otherwise pass silently: it freezes the page rather than failing an assertion.
assert.deepEqual(dialogs, [], "no unexpected native dialog during the race day flow");
} finally {
// Leave autoReconnect off again so later scenarios in the shared harness don't retry
// against a decoder socket that no longer exists.
const cleanup = await savedState();
await patchState({ settings: { ...cleanup.settings, autoReconnect: false } });
await decoder.close();
}
}