Frontend unit tests cover durations, colours, statistics and health (closes #21)
check / check (push) Successful in 3m30s
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
This commit is contained in:
+158
-2
@@ -1,5 +1,6 @@
|
||||
// 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.
|
||||
// Unit tests for src/main.js, run with Node's built-in test runner by the
|
||||
// test script in package.json. Importing the module does not start the
|
||||
// page.
|
||||
|
||||
import { beforeEach, test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
@@ -7,6 +8,10 @@ import {
|
||||
AppState,
|
||||
CONFIG,
|
||||
greyOutUI,
|
||||
HostState,
|
||||
humanDuration,
|
||||
latencyClass,
|
||||
latencyHex,
|
||||
measureLatency,
|
||||
tick,
|
||||
} from "../../src/main.js";
|
||||
@@ -224,3 +229,154 @@ test("at a 30000ms interval, after the user pauses and resumes during a round, n
|
||||
assert.notEqual(statusText(state, host), "paused", host.name);
|
||||
}
|
||||
});
|
||||
|
||||
for (const [seconds, text] of [
|
||||
[0, "0s"],
|
||||
[1, "1s"],
|
||||
[59, "59s"],
|
||||
[60, "1m"],
|
||||
[61, "1m1s"],
|
||||
[3599, "59m59s"],
|
||||
[3600, "1h"],
|
||||
[3601, "1h1s"],
|
||||
[3660, "1h1m"],
|
||||
[3661, "1h1m1s"],
|
||||
]) {
|
||||
test(`humanDuration writes ${seconds} seconds as ${text}`, () => {
|
||||
assert.equal(humanDuration(seconds), text);
|
||||
});
|
||||
}
|
||||
|
||||
// The colour of a target's latency figure, as a class, and of its sparkline
|
||||
// for an answer after latency ms, as the colour coding in README.md gives
|
||||
// them. The rows sit either side of each boundary.
|
||||
for (const [latency, figure, sparkline] of [
|
||||
[0, "text-green-500", "#22c55e"],
|
||||
[49, "text-green-500", "#22c55e"],
|
||||
[50, "text-lime-500", "#84cc16"],
|
||||
[99, "text-lime-500", "#84cc16"],
|
||||
[100, "text-yellow-500", "#eab308"],
|
||||
[199, "text-yellow-500", "#eab308"],
|
||||
[200, "text-orange-500", "#f97316"],
|
||||
[499, "text-orange-500", "#f97316"],
|
||||
[500, "text-red-500", "#ef4444"],
|
||||
]) {
|
||||
test(`an answer after ${latency}ms has a ${figure} figure and a ${sparkline} sparkline`, () => {
|
||||
assert.equal(latencyClass(latency, "online"), figure);
|
||||
assert.equal(latencyHex(latency), sparkline);
|
||||
});
|
||||
}
|
||||
|
||||
test("a check that timed out or found its target unreachable has a grey figure and sparkline", () => {
|
||||
assert.equal(latencyClass(null, "error"), "text-gray-500");
|
||||
assert.equal(latencyClass(null, "offline"), "text-gray-500");
|
||||
assert.equal(latencyHex(null), "#6b7280");
|
||||
});
|
||||
|
||||
// A target whose checks, in turn, answered after each of latencies ms, or,
|
||||
// for null, found it unreachable.
|
||||
function hostAfter(latencies) {
|
||||
const host = new HostState({ name: "Target", url: "https://target.test" });
|
||||
for (const latency of latencies) {
|
||||
host.pushSample(
|
||||
Date.now(),
|
||||
latency === null
|
||||
? { latency: null, error: "unreachable" }
|
||||
: { latency, error: null },
|
||||
);
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
for (const { history, latencies, statistics } of [
|
||||
{
|
||||
history: "no checks",
|
||||
latencies: [],
|
||||
statistics: { min: null, max: null, average: null, median: null },
|
||||
},
|
||||
{
|
||||
history: "only unreachable checks",
|
||||
latencies: [null, null, null],
|
||||
statistics: { min: null, max: null, average: null, median: null },
|
||||
},
|
||||
{
|
||||
history: "three answers and an unreachable check",
|
||||
latencies: [30, null, 10, 20],
|
||||
statistics: { min: 10, max: 30, average: 20, median: 20 },
|
||||
},
|
||||
{
|
||||
// The median of an even number of answers is the mean of the
|
||||
// middle two. It and the average, 23.75, are rounded.
|
||||
history: "four answers",
|
||||
latencies: [10, 40, 20, 25],
|
||||
statistics: { min: 10, max: 40, average: 24, median: 23 },
|
||||
},
|
||||
]) {
|
||||
test(`a target's min, max, average and median latency over ${history}`, () => {
|
||||
const host = hostAfter(latencies);
|
||||
assert.deepEqual(
|
||||
{
|
||||
min: host.minLatency(),
|
||||
max: host.maxLatency(),
|
||||
average: host.averageLatency(),
|
||||
median: host.medianLatency(),
|
||||
},
|
||||
statistics,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// An app state in which, of the WAN targets, the first timedOut timed out,
|
||||
// the next unreachable were found unreachable, the next answered answered
|
||||
// after latency ms, and the rest have not been checked yet.
|
||||
function stateAfter({ timedOut, unreachable, answered, latency }) {
|
||||
const state = new AppState([]);
|
||||
const results = [
|
||||
...Array(timedOut).fill({ latency: null, error: "timeout" }),
|
||||
...Array(unreachable).fill({ latency: null, error: "unreachable" }),
|
||||
...Array(answered).fill({ latency, error: null }),
|
||||
];
|
||||
results.forEach((result, i) => state.wan[i].pushSample(Date.now(), result));
|
||||
return state;
|
||||
}
|
||||
|
||||
// For each number the health is decided by, the rows put it one under, at
|
||||
// and one over its threshold.
|
||||
for (const { timedOut, unreachable = 0, answered, latency, health } of [
|
||||
// Offline: more than 10 timed out and at most 4 answered.
|
||||
{ timedOut: 9, answered: 4, latency: 30, health: "degraded" },
|
||||
{ timedOut: 10, answered: 4, latency: 30, health: "degraded" },
|
||||
{ timedOut: 11, answered: 4, latency: 30, health: "offline" },
|
||||
{ timedOut: 11, answered: 3, latency: 30, health: "offline" },
|
||||
{ timedOut: 11, answered: 5, latency: 30, health: "degraded" },
|
||||
// Otherwise degraded: more than 4 timed out.
|
||||
{ timedOut: 3, answered: 10, latency: 30, health: "healthy" },
|
||||
{ timedOut: 4, answered: 10, latency: 30, health: "healthy" },
|
||||
{ timedOut: 5, answered: 10, latency: 30, health: "degraded" },
|
||||
// Otherwise slow: more than 3 answered after more than 1000ms.
|
||||
{ timedOut: 0, answered: 4, latency: 999, health: "healthy" },
|
||||
{ timedOut: 0, answered: 4, latency: 1000, health: "healthy" },
|
||||
{ timedOut: 0, answered: 4, latency: 1001, health: "slow" },
|
||||
{ timedOut: 0, answered: 2, 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, ${unreachable} found unreachable and ${answered} answering after ${latency}ms, the health is ${health}`, () => {
|
||||
const state = stateAfter({ timedOut, unreachable, answered, latency });
|
||||
assert.equal(state.healthStatus(), health);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user