import assert from "node:assert/strict"; import { startFakeDecoder } from "./browser_fake_decoder.js"; export async function runDecoderReconnectSmoke({ evaluate, navigate, savedState, waitFor, pause, base }) { 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 badgeOnline = () => evaluate('document.getElementById("connectionBadge")?.classList.contains("badge-online")'); const decoder = await startFakeDecoder(); try { const seeded = await savedState(); await patchState({ settings: { ...seeded.settings, autoReconnect: true, wsUrl: `ws://127.0.0.1:${decoder.port}` } }); await navigate("events", "eventForm"); await waitFor(async () => await badgeOnline(), "decoder connects on page load"); assert.equal(decoder.activeConnections(), 1, "exactly one live connection after the initial connect"); assert.equal(decoder.totalConnections(), 1, "a clean initial connect makes exactly one connection attempt"); // Simulate a real network/decoder interruption: drop the TCP connection with a raw // reset rather than a clean WS close handshake, the way a lost cable or a decoder // power-cycle actually would - see src/decoder_runtime.js's wsClient.onclose. decoder.dropConnections(); await waitFor(async () => (await badgeOnline()) === false, "badge shows offline once the drop is detected"); assert.equal(decoder.activeConnections(), 0, "no lingering connection right after the drop"); // decoder_runtime.js schedules exactly one reconnect attempt 2000ms after onclose. await waitFor(async () => decoder.totalConnections() >= 2, "a reconnect attempt reaches the decoder socket"); await waitFor(async () => await badgeOnline(), "badge shows online again after auto-reconnect"); assert.equal(decoder.totalConnections(), 2, "auto-reconnect made exactly one new connection attempt, not several"); assert.equal(decoder.activeConnections(), 1, "auto-reconnect settles on exactly one live connection, not a duplicate"); console.log("PASS decoder auto-reconnects after a dropped socket onto exactly one connection"); // A second drop must not leave a stray reconnect timer running once autoReconnect is // turned off - the retry loop should stop, not silently keep firing in the background. // Toggling it through the real Settings form (not a raw state patch) matters here: // a raw backend patch never reaches this already-loaded page's in-memory state, only // a fresh navigation would, and that would also drop and reopen the live connection. await navigate("settings", "settingsForm"); await waitFor(async () => await badgeOnline(), "decoder is connected on the settings page"); await evaluate('document.querySelector("#settingsForm [name=autoReconnect]").checked = false'); await evaluate('document.getElementById("settingsForm").requestSubmit()'); const connectionsBeforeDrop = decoder.totalConnections(); decoder.dropConnections(); await waitFor(async () => (await badgeOnline()) === false, "badge shows offline after the second drop"); await pause(3000); assert.equal(decoder.totalConnections(), connectionsBeforeDrop, "no reconnect attempt is made once autoReconnect is disabled"); assert.equal(await badgeOnline(), false, "stays offline with autoReconnect disabled"); assert.equal(decoder.activeConnections(), 0, "no duplicate or stray connection appears while disabled"); console.log("PASS disabling auto-reconnect stops further reconnect attempts after a drop"); } finally { const cleanup = await savedState(); await patchState({ settings: { ...cleanup.settings, autoReconnect: false } }); await decoder.close(); } }