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) })