check / check (push) Successful in 1m55s
Each target check now times out after 80% of the refresh interval, 24 seconds at 30 seconds, where it was capped at 3 seconds, so slow, far targets are recorded with their real time. Rounds never overlap: a round gives up the last round's checks if they are still waiting, which happens only when a round starts early, after an interval change or when the recovery probe finds a target answering. The probe still checks every half second, giving up its previous checks, so they do not pile up. The frontend has its first unit tests, run by script/frontend-test with Node's built-in test runner on a mocked clock. index.html now links src/styles.css, which src/main.js imported, since Node cannot import CSS. Model: opus-5-5
67 lines
2.5 KiB
JavaScript
67 lines
2.5 KiB
JavaScript
// Unit tests for src/main.js, run by script/frontend-test with Node's
|
|
// built-in test runner. Importing the module does not start the page.
|
|
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { CONFIG, measureLatency } from "../../src/main.js";
|
|
|
|
// measureLatency writes timeouts to the debug log, which looks for its
|
|
// panel in the page. There is no page here.
|
|
globalThis.document = { getElementById: () => null };
|
|
|
|
// Mocks the clock for test t, so that a check lasting seconds takes no real
|
|
// time, and replaces fetch with a target that answers after answerAfter
|
|
// milliseconds of that clock, or never when answerAfter is Infinity. Both
|
|
// are restored when the test ends.
|
|
function mockTarget(t, answerAfter) {
|
|
t.mock.timers.enable({ apis: ["setTimeout", "Date"] });
|
|
t.mock.method(performance, "now", () => Date.now());
|
|
t.mock.method(
|
|
globalThis,
|
|
"fetch",
|
|
(url, { signal }) =>
|
|
new Promise((resolve, reject) => {
|
|
if (answerAfter !== Infinity) setTimeout(resolve, answerAfter);
|
|
signal.addEventListener("abort", () => reject(signal.reason));
|
|
}),
|
|
);
|
|
}
|
|
|
|
// The result of check if it has ended, otherwise "still waiting".
|
|
function settled(check) {
|
|
return Promise.race([
|
|
check,
|
|
new Promise((resolve) => setImmediate(resolve, "still waiting")),
|
|
]);
|
|
}
|
|
|
|
for (const interval of [10000, 30000]) {
|
|
const timeout = interval * 0.8;
|
|
// Over 3 seconds, which the timeout used to be capped at.
|
|
const slowAnswer = timeout - 1000;
|
|
|
|
test(`at a ${interval}ms interval, an answer after ${slowAnswer}ms is recorded with its real time`, async (t) => {
|
|
CONFIG.updateInterval = interval;
|
|
mockTarget(t, slowAnswer);
|
|
const check = measureLatency("https://target.test");
|
|
t.mock.timers.tick(slowAnswer);
|
|
assert.deepEqual(await settled(check), {
|
|
latency: slowAnswer,
|
|
error: null,
|
|
});
|
|
});
|
|
|
|
test(`at a ${interval}ms interval, a target that never answers is recorded as a timeout after ${timeout}ms`, async (t) => {
|
|
CONFIG.updateInterval = interval;
|
|
mockTarget(t, Infinity);
|
|
const check = measureLatency("https://target.test");
|
|
t.mock.timers.tick(timeout - 1);
|
|
assert.equal(await settled(check), "still waiting");
|
|
t.mock.timers.tick(1);
|
|
assert.deepEqual(await settled(check), {
|
|
latency: null,
|
|
error: "timeout",
|
|
});
|
|
});
|
|
}
|