From 4faf5312ff4caf069b6693337bbab04a7218e003 Mon Sep 17 00:00:00 2001 From: stroblme Date: Mon, 14 Sep 2026 15:02:42 +0200 Subject: [PATCH] Measure the bar where it settles, not mid-spring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `widgets.spec.ts` took a one-shot `boundingBox()` of a fill that is still moving. Two ways that reads wrong. "a bar draws a row per reading" compared two widths that are both zero until the socket has delivered the readings, and a plain `expect` over a captured number does not retry. "a reading over its scale fills the track and no more" allowed the fill one pixel past the track, but the house spring is underdamped: `motion`'s own generator puts the fluksio look at 100.66% of the track 276ms in (material 100.91%, glass 103.58%), which on this tile's ~240px track is ~1.6px — over the tolerance, and reached only if the sample happens to land near the peak. Neither is the widget. `fractionOf` clamps the reading to [0, 1] and `.dui-bar-track` is `overflow: hidden`, so nothing past a full track is ever drawn; a bounding box just is not clipped by an ancestor's overflow. So both now poll: one until the shorter reading is drawn shorter, the other until the fill is flush with its track — which is a stricter reading of "and no more" than the pixel of slack was. The live-socket test gains the other half of what that spin cost: an ASGI task that never returns is one uvicorn's graceful shutdown waits on forever, which is why `fluksio serve` could only be SIGKILLed and left `serve.pid` behind. --- backend/tests/api/routes/test_flows.py | 6 +++ frontend/tests/widgets.spec.ts | 56 +++++++++++++++++--------- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/backend/tests/api/routes/test_flows.py b/backend/tests/api/routes/test_flows.py index c548aa5..5ef9cbc 100644 --- a/backend/tests/api/routes/test_flows.py +++ b/backend/tests/api/routes/test_flows.py @@ -507,6 +507,12 @@ async def test_the_live_socket_ends_when_its_client_goes( `receive_text` raises for good once the peer has gone, so reading it again spun this loop at full tilt: the event loop stalled, `/health` answered 503 and one queue getter was orphaned per turn. + + It is also what made `fluksio serve` unstoppable. A handler that never + returns is an ASGI task uvicorn never sees finish, and its graceful + shutdown waits on those with no timeout — so SIGTERM logged "Waiting for + background tasks to complete" and nothing else, leaving `serve.pid` behind + because only SIGKILL ended the process. This returning is what ends that. """ monkeypatch.setattr(flows_route, "user_from_token", lambda session, token: object()) diff --git a/frontend/tests/widgets.spec.ts b/frontend/tests/widgets.spec.ts index cab717b..a6f5e84 100644 --- a/frontend/tests/widgets.spec.ts +++ b/frontend/tests/widgets.spec.ts @@ -312,14 +312,24 @@ test("a bar draws a row per reading", async ({ page }) => { await expect(rows).toHaveCount(2) const fills = page.getByTestId("bar-fill") - const outer = await fills.nth(0).boundingBox() - const inner = await fills.nth(1).boundingBox() - expect(outer).not.toBeNull() - expect(inner).not.toBeNull() - expect( - inner!.width, - "the smaller reading is not drawn as the shorter bar", - ).toBeLessThan(outer!.width) + const width = (index: number) => + fills + .nth(index) + .boundingBox() + .then((box) => box?.width ?? 0) + // Both fills spring up from zero once the reading lands, so a single + // measurement can be taken before the socket has delivered either — two + // widths of nothing, where "shorter" is not true of anything. + await expect + .poll( + async () => { + const outer = await width(0) + const inner = await width(1) + return inner > 0 && inner < outer + }, + { message: "the smaller reading is not drawn as the shorter bar" }, + ) + .toBe(true) // Told apart by colour rather than by nesting, which is what lets a row // carry a scale of its own. @@ -338,16 +348,26 @@ test("a reading over its scale fills the track and no more", async ({ await publish(page, w("pv"), 120) await expect(page.getByText(/120\.00? kW/)).toBeVisible() - const fill = await page.getByTestId("bar-fill").nth(1).boundingBox() - const track = await page - .getByTestId("bar-row") - .nth(1) - .locator(".dui-bar-track") - .boundingBox() - expect( - fill!.width, - "a value over the scale runs off the end of its track", - ).toBeLessThanOrEqual(track!.width + 1) + // `fractionOf` clamps the reading to the scale and the track clips what is + // drawn, so nothing past a full track is ever *shown*. The fill element is + // sprung there, and the house spring is underdamped — it reaches 100.7% of + // the track about 280ms in, clipped and invisible but measurable, because a + // bounding box is not clipped by an ancestor's overflow. Where it settles is + // what "fills the track and no more" is about, so that is what is measured: + // flush with the track, neither short of it nor over. + const track = page.getByTestId("bar-row").nth(1).locator(".dui-bar-track") + await expect + .poll( + async () => { + const fill = await page.getByTestId("bar-fill").nth(1).boundingBox() + const rail = await track.boundingBox() + return Math.round(fill!.width - rail!.width) + }, + { + message: "a value over the scale does not settle flush with its track", + }, + ) + .toBe(0) await publish(page, w("pv"), 30) })