Measure the bar where it settles, not mid-spring

`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.
This commit is contained in:
2026-09-14 15:02:42 +02:00
parent 94c9260452
commit 4faf5312ff
2 changed files with 44 additions and 18 deletions
+6
View File
@@ -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())
+38 -18
View File
@@ -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)
})