1 Commits
Author SHA1 Message Date
sneak 6126a7f85c Each target's row shows its result as soon as its check ends (closes #91)
check / check (push) Successful in 1m57s
tick drew no row until the round's slowest check ended, up to 24
seconds at a 30-second interval since checks time out at 80% of it.
Each check now pushes its sample and redraws its row as it ends. When
the last check ends, every row is redrawn, so none still reads "paused"
after a pause and resume, and sorting, the summary, the health box and
offline detection run once. A check that ends while paused or after its
round is given up draws nothing, and the first round is still discarded
as a whole. The row is looked up when the check ends, as a pin click can
re-sort the rows mid-round.

Unit tests run tick on the mocked clock against a stand-in page.

Model: opus-5-5
2026-10-03 15:07:54 +00:00
4 changed files with 73 additions and 12 deletions
+3 -3
View File
@@ -105,9 +105,9 @@ code lives in `src/main.js` with a class-based architecture:
- **UI functions**: `buildUI()` constructs the DOM, `updateHostRow()` / - **UI functions**: `buildUI()` constructs the DOM, `updateHostRow()` /
`updateSummary()` / `updateHealthBox()` handle incremental updates `updateSummary()` / `updateHealthBox()` handle incremental updates
- **`tick()`**: Main loop — measures all hosts in parallel, pushing each host's - **`tick()`**: Main loop — measures all hosts in parallel, pushing each host's
sample and redrawing its row as soon as its check ends, then sorts and redraws sample and redrawing its row as soon as its check ends, then redraws every
the summary and health box once the last check ends. When paused, pushes blank row, sorts, and redraws the summary and health box once the last check ends.
markers (no probes, no false outage) When paused, pushes blank markers (no probes, no false outage)
- **`Reporter`**: Posts collected samples to the backend - **`Reporter`**: Posts collected samples to the backend
### Reporting ### Reporting
+5 -4
View File
@@ -25,10 +25,11 @@ latest run passes.
- 2026-10-03: each target's row shows its result as soon as its check ends - 2026-10-03: each target's row shows its result as soon as its check ends
(issue #91), where every row waited for the round's slowest check, up to 24 (issue #91), where every row waited for the round's slowest check, up to 24
seconds at a 30-second interval. Sorting, the summary, the health box and seconds at a 30-second interval. Every row is still redrawn, and sorting, the
offline detection still run once, when the round's last check ends. A check summary, the health box and offline detection still run, once, when the
that ends after the user pauses or after its round is given up shows nothing, round's last check ends, so no row reads "paused" after a pause and resume
and the first round is still discarded as a whole during the round. A check that ends after the user pauses or after its round
is given up shows nothing, and the first round is still discarded as a whole
- 2026-10-03: the Go tests run with the race detector and coverage (issue #88): - 2026-10-03: the Go tests run with the race detector and coverage (issue #88):
`backend/script/test` runs `go test -timeout 30s -race -cover ./...` and, if `backend/script/test` runs `go test -timeout 30s -race -cover ./...` and, if
that fails, runs it again with `-v` and fails. Go's `-timeout` bounds the that fails, runs it again with `-v` and fails. Go's `-timeout` bounds the
+4
View File
@@ -1156,6 +1156,10 @@ export async function tick(state, signal, onOffline) {
return; return;
} }
// Redraw every row: if the user paused and resumed during this round,
// rows whose check ended before the resume still read "paused"
state.allHosts.forEach((host, i) => updateHostRow(host, i));
// Sort after the first real check, then every 10 ticks thereafter // Sort after the first real check, then every 10 ticks thereafter
if (state.tickCount === 2 || state.tickCount % 10 === 1) { if (state.tickCount === 2 || state.tickCount % 10 === 1) {
sortAndRebuildWAN(state); sortAndRebuildWAN(state);
+61 -5
View File
@@ -1,16 +1,20 @@
// Unit tests for src/main.js, run by script/frontend-test with Node's // 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. // built-in test runner. Importing the module does not start the page.
import { test } from "node:test"; import { beforeEach, test } from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { AppState, CONFIG, measureLatency, tick } from "../../src/main.js"; import { AppState, CONFIG, measureLatency, tick } from "../../src/main.js";
// There is no page here, so the tests stand in for it. The debug log looks // There is no page here, so the tests stand in for it. The debug log looks
// for its panel by id and finds none. Each element of a host's row that // for its panel by id and finds none. Each element of a host's row that
// tick draws into is a plain object, made the first time it is looked up // tick draws into is a plain object, made the first time a test looks it
// and kept in elements under its selector. Drawing a sparkline does // up and kept in elements under its selector until the next test starts.
// nothing; it looks for the pixel ratio on window and finds none. // Drawing a sparkline does nothing; it looks for the pixel ratio on window
const elements = {}; // and finds none.
let elements;
beforeEach(() => {
elements = {};
});
const doNothing = () => {}; const doNothing = () => {};
const canvasContext = { const canvasContext = {
clearRect: doNothing, clearRect: doNothing,
@@ -120,3 +124,55 @@ test("at a 30000ms interval, a target answering after 1000ms shows in its row wh
assert.notEqual(await settled(round), "still waiting"); assert.notEqual(await settled(round), "still waiting");
assert.equal(state.tickCount, 3); assert.equal(state.tickCount, 3);
}); });
// In the next three tests, the answering target's check is still waiting
// when something happens after which its result must not show.
test("at a 30000ms interval, a check still waiting when its round is given up does not show in its row", async (t) => {
CONFIG.updateInterval = 30000;
const state = new AppState([
{ name: "Answering", url: "https://answering.test" },
]);
const answering = state.local[0];
mockTargets(t, (url) => (url.startsWith(answering.url) ? 1000 : Infinity));
state.tickCount = 2;
const roundChecks = new AbortController();
const round = tick(state, roundChecks.signal);
t.mock.timers.tick(500);
// As a round started early does to the last round's checks.
roundChecks.abort();
assert.notEqual(await settled(round), "still waiting");
assert.equal(latencyFigure(state, answering), undefined);
});
test("at a 30000ms interval, a check still waiting when the user pauses does not show in its row", async (t) => {
CONFIG.updateInterval = 30000;
const state = new AppState([
{ name: "Answering", url: "https://answering.test" },
]);
const answering = state.local[0];
mockTargets(t, (url) => (url.startsWith(answering.url) ? 1000 : Infinity));
state.tickCount = 2;
const round = tick(state, new AbortController().signal);
t.mock.timers.tick(500);
state.paused = true;
t.mock.timers.tick(500);
assert.equal(await settled(round), "still waiting");
assert.equal(latencyFigure(state, answering), undefined);
});
test("at a 30000ms interval, a check in the first round does not show in its row", async (t) => {
CONFIG.updateInterval = 30000;
const state = new AppState([
{ name: "Answering", url: "https://answering.test" },
]);
const answering = state.local[0];
mockTargets(t, (url) => (url.startsWith(answering.url) ? 1000 : Infinity));
const round = tick(state, new AbortController().signal);
t.mock.timers.tick(1000);
assert.equal(await settled(round), "still waiting");
assert.equal(latencyFigure(state, answering), undefined);
});