1 Commits
Author SHA1 Message Date
sneak 2c4aac4719 Frontend unit tests cover durations, colours, statistics and health (closes #21)
check / check (push) Successful in 3m30s
New table-driven tests in test/unit/main.test.js: humanDuration, the
figure and sparkline colours either side of each latency boundary, a
target's min, max, average and median over an empty history, an
all-unreachable one, a mixed one and one of only answers, and the four
health states either side of their thresholds, with targets found
unreachable counted as timed out. src/main.js exports the four names
they need.

package.json gains a test script, which script/frontend-test runs with
the dot reporter and, if a test fails, again with the spec reporter
before failing. NODE_OPTIONS picks the reporter, since yarn appends its
arguments after the test files.

Model: opus-5-5
2026-10-04 00:34:52 +00:00
+28 -14
View File
@@ -327,23 +327,22 @@ for (const { history, latencies, statistics } of [
} }
// An app state in which, of the WAN targets, the first timedOut timed out, // An app state in which, of the WAN targets, the first timedOut timed out,
// the next answered answered after latency ms, and the rest have not been // the next unreachable were found unreachable, the next answered answered
// checked yet. // after latency ms, and the rest have not been checked yet.
function stateAfter({ timedOut, answered, latency }) { function stateAfter({ timedOut, unreachable, answered, latency }) {
const state = new AppState([]); const state = new AppState([]);
for (const host of state.wan.slice(0, timedOut)) { const results = [
host.pushSample(Date.now(), { latency: null, error: "timeout" }); ...Array(timedOut).fill({ latency: null, error: "timeout" }),
} ...Array(unreachable).fill({ latency: null, error: "unreachable" }),
for (const host of state.wan.slice(timedOut, timedOut + answered)) { ...Array(answered).fill({ latency, error: null }),
host.pushSample(Date.now(), { latency, error: null }); ];
} results.forEach((result, i) => state.wan[i].pushSample(Date.now(), result));
return state; return state;
} }
// For each number the health is decided by, the rows put it one under, at // For each number the health is decided by, the rows put it one under, at
// and one over its threshold. A target found unreachable counts as one // and one over its threshold.
// that timed out. for (const { timedOut, unreachable = 0, answered, latency, health } of [
for (const { timedOut, answered, latency, health } of [
// Offline: more than 10 timed out and at most 4 answered. // Offline: more than 10 timed out and at most 4 answered.
{ timedOut: 9, answered: 4, latency: 30, health: "degraded" }, { timedOut: 9, answered: 4, latency: 30, health: "degraded" },
{ timedOut: 10, answered: 4, latency: 30, health: "degraded" }, { timedOut: 10, answered: 4, latency: 30, health: "degraded" },
@@ -360,9 +359,24 @@ for (const { timedOut, answered, latency, health } of [
{ timedOut: 0, answered: 4, latency: 1001, health: "slow" }, { timedOut: 0, answered: 4, latency: 1001, health: "slow" },
{ timedOut: 0, answered: 2, latency: 1001, health: "healthy" }, { timedOut: 0, answered: 2, latency: 1001, health: "healthy" },
{ timedOut: 0, answered: 3, latency: 1001, health: "healthy" }, { timedOut: 0, answered: 3, latency: 1001, health: "healthy" },
// A target found unreachable counts as one that timed out.
{
timedOut: 5,
unreachable: 6,
answered: 4,
latency: 30,
health: "offline",
},
{
timedOut: 0,
unreachable: 5,
answered: 10,
latency: 30,
health: "degraded",
},
]) { ]) {
test(`with ${timedOut} WAN targets timed out and ${answered} answering after ${latency}ms, the health is ${health}`, () => { test(`with ${timedOut} WAN targets timed out, ${unreachable} found unreachable and ${answered} answering after ${latency}ms, the health is ${health}`, () => {
const state = stateAfter({ timedOut, answered, latency }); const state = stateAfter({ timedOut, unreachable, answered, latency });
assert.equal(state.healthStatus(), health); assert.equal(state.healthStatus(), health);
}); });
} }