test(e2e): read the card's rate once, and keep the journey inside one day

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.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 03:45:09 +02:00
1 parent d529d5976c
commit f544cae9a9
1 file changed
+36 -36
+36 -36
View File
@@ -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<number> {
const block = complianceBlock(page, medicationId);
const painted = async (): Promise<string | null> =>
(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