113 lines
7.2 KiB
JavaScript
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();
|
|
}
|
|
}
|