From f544cae9a978c55c5bb207ecf632cb23a46d1c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 10 Sep 2026 03:45:09 +0200 Subject: [PATCH] test(e2e): read the card's rate once, and keep the journey inside one day MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two separate ways the flows could not deliver what they promised. The card read polled for up to seventy seconds and escaped a missing figure by reloading the page — which re-mounts the list and issues the very batched compliance call it was waiting out, so each poll spent one more of the allowance it was short of. Meanwhile the budget is thirty seconds tripled by `test.slow()`, so one ride-out consumed the whole test and the run died on a Playwright timeout rather than on the "the card never painted an adherence figure" message the note promised. The inner poll was also wrapped in a second one with a thirty-second budget, and `expect.poll` does not abort an in-flight callback, so a seventy-second call ran to completion inside it. With the journey on its own account there is nothing to ride out: the read now waits twenty seconds for the attribute and fails with the diagnosis attached. The nesting is gone too — the step that waits for the card to re-read its schedule polls the attribute directly. The other one is the day boundary. A clock-anchored slot is inside its own on-time window by construction, so the TAKE is always today's; the dashboard assertion is not, because it reads the latest day and a day's expected count carries no now-cap. Cross midnight between the take and the final read and the latest day is a fresh one with two doses scheduled and none taken, which answers 0 where 50 is asserted. The flow is a couple of minutes long, so it declines to start in the last ten minutes of the day and says why. --- e2e/medication-compliance-journey.spec.ts | 72 +++++++++++------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/e2e/medication-compliance-journey.spec.ts b/e2e/medication-compliance-journey.spec.ts index 85af6be0a..4ecacfbbd 100644 --- a/e2e/medication-compliance-journey.spec.ts +++ b/e2e/medication-compliance-journey.spec.ts @@ -178,7 +178,8 @@ function cacheFlusher(page: Page, medicationId: string, dose: string) { function complianceBlock(page: Page, medicationId: string) { return page .locator(`[data-medication-id="${medicationId}"]`) - .locator('[data-slot="medication-card-compliance"]'); + .locator('[data-slot="medication-card-compliance"]') + .first(); } /** @@ -186,40 +187,18 @@ function complianceBlock(page: Page, medicationId: string) { * * The rate's presence is the gate: the skeleton and the quiet error fallback * reserve the same footprint as the loaded bars and carry no rates, so an - * attribute that is there is a figure the card really painted. - * - * The reload is for one specific state. The batched compliance route allows - * thirty reads a minute per account, one list visit spends several, and a - * refused read paints the error fallback rather than the bars — so a repeated - * local run, or CI retrying the whole serial group inside the same minute, can - * meet a card with no figure on it for reasons that have nothing to do with - * adherence. Re-reading rides that out without hiding anything: if the figure - * never arrives, this still fails. + * attribute that is there is a figure the card really painted. Twenty seconds + * is the list's own load-plus-fetch budget and nothing more — the account is + * this spec's own, so a card with no figure on it is a defect rather than a + * neighbour's spent read allowance. */ async function readCardRate(page: Page, medicationId: string): Promise { const block = complianceBlock(page, medicationId); - const painted = async (): Promise => - (await block.count()) === 0 - ? null - : block.first().getAttribute("data-rate-short"); - - await expect - .poll( - async () => { - const first = await painted(); - if (first !== null) return first; - await page.reload(); - return painted(); - }, - { - timeout: 70_000, - intervals: [500, 1_000, 2_000, 5_000, 10_000], - message: "the card never painted an adherence figure", - }, - ) - .toMatch(/^\d+$/); - - return Number(await painted()); + await expect( + block, + "the card never painted an adherence figure", + ).toHaveAttribute("data-rate-short", /^\d+$/, { timeout: 20_000 }); + return Number(await block.getAttribute("data-rate-short")); } /** The dashboard tile's most recent day, as the tile itself computed it. */ @@ -368,6 +347,20 @@ test.describe("medication adherence journey", () => { // on a loaded runner that settle earns the tripled timeout. test.slow(); + // The tile reads THE LATEST day, and a day boundary crossed mid-flow moves + // that day out from under both tile assertions: a fresh day has two doses + // scheduled, none taken, and answers 0 where 50 is asserted. The flow is a + // couple of minutes long, so declining to start in the last ten of the day + // is enough. The clock is the browser's, which the config pins to the zone + // the compliance engine falls back to. + const minutesLeftInDay = await page.evaluate( + () => 24 * 60 - (new Date().getHours() * 60 + new Date().getMinutes()), + ); + test.skip( + minutesLeftInDay < 10, + "starts inside the last ten minutes of the day — the dashboard tile's latest day would change under the flow", + ); + const { id: medicationId } = await createTwiceDailyMedication( page, "E2E Adhärenz", @@ -405,8 +398,15 @@ test.describe("medication adherence journey", () => { // the dose just taken instead of recording a second one. The rate leaving // zero is the card's own confirmation that the refetch landed. await expect - .poll(() => readCardRate(page, medicationId), { timeout: 30_000 }) - .toBeGreaterThan(0); + .poll( + () => + complianceBlock(page, medicationId).getAttribute("data-rate-short"), + { + timeout: 30_000, + message: "the card never re-read its schedule after the take", + }, + ) + .toMatch(/^[1-9]\d*$/); const skipWritten = page.waitForResponse( (res) => new URL(res.url()).pathname === intakePath && @@ -526,9 +526,9 @@ test.describe("medication adherence journey", () => { test("a two-weekday plan does not count the other five days as missed", async ({ page, }) => { - // Room for the reload budget in `readCardRate` (see its note). + // Two medications seeded, aged and dosed three times apiece before the + // first assertion; on a loaded runner that earns the tripled timeout. test.slow(); - await openSeedingSurface(page); // Both dose days are behind us and neither is today, so the plan's whole // realised history is closed and the verdicts do not depend on the hour