race day tester + fixa open practice start och decoder-omritning

Nytt browser-test för start/paus/fortsätt/finish och byte till
nästa heat i kön, mot en simulerad decoder-socket (ingen riktig
AMMC-hårdvara). Hittade och fixade två buggar under vägen:

- Open Practice på sponsor/track-event kunde inte startas trots
  grönt ljus i förkontrollen (separat, striktare kontroll saknade
  samma undantag)
- Race Day-vyn ritades aldrig om när decodern anslöt/kopplades ner

Testramverket hanterar nu även native alert()/confirm()-dialoger
automatiskt, annars fryser en headless körning helt.
This commit is contained in:
larssand
2026-09-09 06:59:54 +02:00
parent b3bec531de
commit acab99ed4d
6 changed files with 188 additions and 5 deletions

View File

@@ -1,5 +1,6 @@
import { runOverlaySmoke } from "./browser_overlays.js";
import { runRaceSetupSmoke } from "./browser_race_setup.js";
import { runRaceDaySmoke } from "./browser_race_day.js";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { existsSync, mkdtempSync, rmSync } from "node:fs";
@@ -42,7 +43,7 @@ try {
const base = `http://127.0.0.1:${port}`;
launch(process.execPath, ["server.js"], { env: { ...process.env, HOST: "127.0.0.1", PORT: String(port), RC_TIMING_DATA_DIR: join(temporary, "data"), RC_TIMING_PID_FILE: "" } });
await waitFor(async () => (await fetch(base + "/api/health")).ok, "isolated server");
const initial = { classes: [{ id: "test_class", name: "Test Class" }], drivers: [], cars: [], events: [], sessions: [], resultsBySession: {}, settings: { language: "en", autoReconnect: false, backendUrl: base }, training: {} };
const initial = { classes: [{ id: "test_class", name: "Test Class" }], drivers: [], cars: [], events: [], sessions: [], resultsBySession: {}, settings: { language: "en", autoReconnect: false, backendUrl: base, raceAudio: { enabled: false } }, training: {} };
const seedResponse = await fetch(base + "/api/state", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(initial) });
assert.equal(seedResponse.ok, true);
launch(chrome, ["--headless", "--disable-gpu", "--no-first-run", "--no-default-browser-check", "--disable-background-networking", ...(process.env.RC_BROWSER_NO_SANDBOX === "1" ? ["--no-sandbox"] : []), `--user-data-dir=${join(temporary, "browser")}`, `--remote-debugging-port=${debugPort}`, "about:blank"]);
@@ -53,6 +54,7 @@ try {
let nextId = 0;
const requests = new Map();
const errors = [];
const dialogs = [];
socket.addEventListener("message", event => {
const message = JSON.parse(event.data);
if (message.id) {
@@ -60,6 +62,13 @@ try {
requests.delete(message.id);
if (message.error) request?.reject(new Error(message.error.message)); else request?.resolve(message.result);
} else if (message.method === "Runtime.exceptionThrown") errors.push(message.params.exceptionDetails.text + ": " + (message.params.exceptionDetails.exception?.description || ""));
// A native confirm()/alert() blocks the page's JS thread until dismissed. Nothing in a
// headless run can click it, so every scenario would hang forever on the first one;
// auto-accept and record it so an unexpected dialog still surfaces as a finding.
else if (message.method === "Page.javascriptDialogOpening") {
dialogs.push({ type: message.params.type, message: message.params.message });
send("Page.handleJavaScriptDialog", { accept: true });
}
});
function send(method, params = {}) {
return new Promise((resolve, reject) => { const id = ++nextId; requests.set(id, { resolve, reject }); socket.send(JSON.stringify({ id, method, params })); });
@@ -108,6 +117,7 @@ try {
console.log("PASS create/edit/reload and draft retention: " + collection);
}
await runRaceSetupSmoke({ evaluate, navigate, fill, savedState, waitFor, pause });
await runRaceDaySmoke({ evaluate, navigate, fill, savedState, waitFor, pause, base, dialogs });
await navigate("classes", "classForm");
await send("Network.emulateNetworkConditions", { offline: true, latency: 0, downloadThroughput: -1, uploadThroughput: -1 });
await fill("classForm", { name: "Offline Draft" });
@@ -150,6 +160,7 @@ try {
await waitFor(async () => await evaluate('document.getElementById("publicTrainingConnection")?.dataset.state === "current"'), "mobile retry recovered");
console.log("PASS mobile offline status preserves profile nodes and retry recovers");
assert.deepEqual(errors, []);
if (dialogs.length) console.log("NOTE auto-accepted native dialog(s):", JSON.stringify(dialogs));
console.log("PASS offline save failure, local draft retention, retry and confirmed saved status");
} finally {
socket?.close();