decoder reconnect
This commit is contained in:
59
tests/browser_decoder_reconnect.js
Normal file
59
tests/browser_decoder_reconnect.js
Normal file
@@ -0,0 +1,59 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,9 @@ export async function startFakeDecoder() {
|
||||
// plain server.close() would wait forever for them to end; track and destroy them
|
||||
// explicitly instead. closeAllConnections() does not reach upgraded sockets.
|
||||
const sockets = new Set();
|
||||
let totalConnections = 0;
|
||||
server.on("upgrade", (req, socket) => {
|
||||
totalConnections += 1;
|
||||
sockets.add(socket);
|
||||
socket.on("close", () => sockets.delete(socket));
|
||||
const key = req.headers["sec-websocket-key"];
|
||||
@@ -32,6 +34,12 @@ export async function startFakeDecoder() {
|
||||
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(port, "127.0.0.1", resolve); });
|
||||
return {
|
||||
port,
|
||||
activeConnections: () => sockets.size,
|
||||
totalConnections: () => totalConnections,
|
||||
// Drops every currently-open socket with a raw TCP reset (not a clean WS close
|
||||
// handshake) to simulate a decoder/network interruption, while the server keeps
|
||||
// listening so the frontend's own reconnect logic has somewhere to reconnect to.
|
||||
dropConnections: () => { for (const socket of sockets) socket.destroy(); },
|
||||
close: () => new Promise(resolve => {
|
||||
for (const socket of sockets) socket.destroy();
|
||||
server.close(() => resolve());
|
||||
|
||||
@@ -2,6 +2,7 @@ import { runOverlaySmoke } from "./browser_overlays.js";
|
||||
import { runRaceSetupSmoke } from "./browser_race_setup.js";
|
||||
import { runRaceDaySmoke } from "./browser_race_day.js";
|
||||
import { runObsPopupSmoke } from "./browser_obs_popup.js";
|
||||
import { runDecoderReconnectSmoke } from "./browser_decoder_reconnect.js";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
||||
@@ -134,6 +135,7 @@ try {
|
||||
await runRaceSetupSmoke({ evaluate, navigate, fill, savedState, waitFor, pause });
|
||||
await runRaceDaySmoke({ evaluate, navigate, fill, savedState, waitFor, pause, base, dialogs });
|
||||
await runObsPopupSmoke({ evaluate, navigate, fill, savedState, waitFor, pause, base, debugPort, dialogs, realClick });
|
||||
await runDecoderReconnectSmoke({ evaluate, navigate, savedState, waitFor, pause, base });
|
||||
await navigate("classes", "classForm");
|
||||
await send("Network.emulateNetworkConditions", { offline: true, latency: 0, downloadThroughput: -1, uploadThroughput: -1 });
|
||||
await fill("classForm", { name: "Offline Draft" });
|
||||
|
||||
Reference in New Issue
Block a user