Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: the refresh picker shows a spinner in place of its clock while its page is fetching

usePolled reports when a request is in flight (through a new onBusy of the
poller) and holds it on for at least 0.4 seconds, so a quick answer still
reads as a refresh. The picker swaps its clock for the spinner, in the same
box so its width stays, and the spinner stands still where motion is off.
Every page with a picker passes it; a page with several polled requests shows
it while any of them runs. The Jobs page fetches through shared components
and has none yet.
This commit is contained in:
iris 2026-11-05 23:09:07 +01:00
commit 47c447409b
2 changed files with 296 additions and 23 deletions

View file

@ -0,0 +1,77 @@
// What the refresh spinner shows: on at once, off only after a minimum time.
import assert from "node:assert/strict";
import { test } from "node:test";
import { createBusyHold } from "../src/ui/polled/busy-hold.ts";
// A clock and a timer the test moves by hand.
function rig() {
let t = 0;
const timers = [];
const shown = [];
const hold = createBusyHold({
show: (s) => shown.push(s),
minMs: 400,
now: () => t,
after: (fn, ms) => {
const timer = { fn, at: t + ms, live: true };
timers.push(timer);
return timer;
},
cancel: (timer) => {
timer.live = false;
},
});
const advance = (ms) => {
t += ms;
for (const timer of timers) {
if (timer.live && timer.at <= t) {
timer.live = false;
timer.fn();
}
}
};
return { hold, shown, advance };
}
test("it turns on at once and, for a quick answer, off only after the minimum", () => {
const { hold, shown, advance } = rig();
hold.busy(true);
assert.deepEqual(shown, [true]);
advance(50);
hold.busy(false);
assert.deepEqual(shown, [true]);
advance(349);
assert.deepEqual(shown, [true]);
advance(1);
assert.deepEqual(shown, [true, false]);
});
test("a slow answer turns it off as soon as it arrives", () => {
const { hold, shown, advance } = rig();
hold.busy(true);
advance(900);
hold.busy(false);
assert.deepEqual(shown, [true, false]);
});
test("a new request while it is held keeps it on and cancels the pending off", () => {
const { hold, shown, advance } = rig();
hold.busy(true);
advance(100);
hold.busy(false);
hold.busy(true);
advance(1000);
assert.deepEqual(shown, [true]);
hold.busy(false);
assert.deepEqual(shown, [true, false]);
});
test("stop drops a pending off, and busy(false) with nothing shown does nothing", () => {
const { hold, shown, advance } = rig();
hold.busy(false);
hold.busy(true);
hold.busy(false);
hold.stop();
advance(1000);
assert.deepEqual(shown, [true]);
});

View file

@ -92,3 +92,25 @@ test("the answer of a dropped request that fails is not reported", async () => {
await flush();
assert.deepEqual(sink.seen.error, []);
});
test("onBusy follows a request in flight, however it ends", async () => {
const { calls, fetch } = manualFetch();
const sink = collect();
const busy = [];
const poller = createPoller({ fetch, ...sink, onBusy: (b) => busy.push(b) });
poller.tick();
poller.tick();
assert.deepEqual(busy, [true]);
calls[0].resolve("one");
await flush();
assert.deepEqual(busy, [true, false]);
poller.tick();
calls[1].reject(new Error("boom"));
await flush();
assert.deepEqual(busy, [true, false, true, false]);
poller.tick();
poller.stop();
assert.deepEqual(busy, [true, false, true, false, true, false]);
poller.stop();
assert.equal(busy.length, 6);
});