Poll on real conditions in the e2e suite and un-age the expiry fixtures

- account-sharing polls the offline persister's own IndexedDB store for
  the flushed snapshot instead of sleeping past the debounce.
- The record-session-fence cases poll for the observable halves of their
  setups (the switch round-trip landing, the exit switch being issued);
  the one remaining fixed window is a documented absence-assertion grace
  with nothing to poll on.
- The documents suite's two remaining sleeps are named for what they
  are: a focus-dance grace the outer document cannot observe, and the
  long-press gesture duration itself.
- The inventory-date fixtures derive their printed expiry from now
  instead of a fixed 2027 date that would silently flip the fixture to
  expired when the calendar catches up.
This commit is contained in:
Marc-André Bombeck committed 2026-08-14 06:17:34 +02:00
1 parent 97cb63a77f
commit 7611c82ae7
4 files changed
+86 -11

No files matched your search

+37 -2
View File
@@ -363,9 +363,44 @@ test.describe("account sharing", () => {
// Let the offline persister flush the delegate's own dashboard to
// IndexedDB (1 s debounce). That snapshot is the stale-paint source this
// whole assertion is aimed at — without it the check would pass against a
// disk that was empty anyway.
// disk that was empty anyway. Poll the persister's own store for the
// written snapshot instead of sleeping past the debounce (C3): the
// condition IS the flush, so a slow CI box waits exactly as long as it
// needs to and a fast one moves on immediately.
await page.goto("/");
await page.waitForTimeout(2000);
await expect
.poll(
() =>
page.evaluate(
() =>
new Promise<boolean>((resolve) => {
const req = indexedDB.open("healthlog-query-cache", 1);
req.onerror = () => resolve(false);
req.onsuccess = () => {
const db = req.result;
try {
const get = db
.transaction("kv", "readonly")
.objectStore("kv")
.get("react-query");
get.onsuccess = () => {
db.close();
resolve(get.result !== undefined);
};
get.onerror = () => {
db.close();
resolve(false);
};
} catch {
db.close();
resolve(false);
}
};
}),
),
{ timeout: 10_000 },
)
.toBe(true);
await openSwitcher(page);
await page.locator('[data-slot="account-switcher-entry"]').click();
+7
View File
@@ -559,6 +559,11 @@ test.describe("document vault", () => {
// to the dialog after Chromium's PDF viewer grabs it, so Escape works
// regardless of when the user presses it.
await expect(dialog.locator("iframe")).toBeVisible();
// True grace (C3): Chromium's PDF viewer steals focus from inside the
// iframe and the sheet hands it back — both moves are invisible to the
// outer document (activeElement is "the iframe" before AND after the
// grab), so there is no condition to poll on. The window only needs to
// outlive the focus dance.
await page.waitForTimeout(250);
await page.keyboard.press("Escape");
await expect(dialog).not.toBeVisible();
@@ -573,6 +578,8 @@ test.describe("document vault", () => {
await expect(button).toBeVisible();
await button.dispatchEvent("touchstart");
// Not a wait-for-condition sleep (C3): this IS the gesture — the finger
// stays down past the long-press threshold before lifting.
await page.waitForTimeout(700);
await button.dispatchEvent("touchend");
+24 -2
View File
@@ -326,11 +326,19 @@ test.describe.serial("FENCE record-session fence in the browser", () => {
const accountId = await entry.getAttribute("data-account-id");
expect(accountId).not.toBeNull();
let switchCommitted = false;
await initiator.route("**/api/account/switch", async (route) => {
await route.fetch();
switchCommitted = true;
await route.abort();
});
await entry.click({ noWaitAfter: true });
// Poll for the server round-trip having landed (the observable half of
// the setup), THEN hold a short fixed window — this one is a true
// absence-assertion grace (C3): "the peer never painted guessed state"
// has no completion signal to poll on, only time in which the wrong
// paint could have happened.
await expect.poll(() => switchCommitted, { timeout: 20_000 }).toBe(true);
await initiator.waitForTimeout(500);
// The peer must never paint owner-ready state it guessed at …
@@ -386,13 +394,19 @@ test.describe.serial("FENCE record-session fence in the browser", () => {
// Hold the response, close the tab while it is in flight. The server has
// committed; nothing will ever broadcast the commit.
const held = deferred();
// The commit is observable: flag once the server round-trip inside the
// route handler finishes, and poll for it instead of a fixed sleep
// (C3) — the tab must close AFTER the write landed, and this waits
// exactly that long.
let switchCommitted = false;
await initiator.route("**/api/account/switch", async (route) => {
await route.fetch();
switchCommitted = true;
await held.promise;
await route.abort();
});
await entry.click({ noWaitAfter: true });
await initiator.waitForTimeout(300);
await expect.poll(() => switchCommitted, { timeout: 20_000 }).toBe(true);
held.release();
await initiator.close();
@@ -591,10 +605,18 @@ test.describe.serial("FENCE record-session fence in the browser", () => {
expect(held.scope).toBe(accountId);
// Now move the context out from under it, and only then let it land.
// "Out from under it" is observable: the exit posts
// `/api/account/switch`, so poll for that request having been issued
// instead of sleeping and hoping the click got that far (C3).
let exitSwitchIssued = false;
await page.route("**/api/account/switch", async (route) => {
exitSwitchIssued = true;
await route.continue();
});
const leaving = page
.locator('[data-slot="shared-record-banner-exit"]')
.click({ noWaitAfter: true });
await page.waitForTimeout(500);
await expect.poll(() => exitSwitchIssued, { timeout: 20_000 }).toBe(true);
held.release?.();
await leaving.catch(() => {});
@@ -18,6 +18,17 @@ import { renderToStaticMarkup } from "react-dom/server";
import { I18nProvider } from "@/lib/i18n/context";
// Now-anchored future expiry (the fixed 2027-06-01 was a fuse: once the
// calendar passes it, an "expiring later" fixture silently becomes an
// expired one and the assertions rot). ~300 days out keeps it a plainly
// future date on every run; the expected strings derive from the same
// instant so they can never drift from the fixture.
const FUTURE_EXPIRY = new Date(Date.now() + 300 * 24 * 60 * 60 * 1000);
const FUTURE_EXPIRY_YMD = FUTURE_EXPIRY.toISOString().slice(0, 10);
const FUTURE_EXPIRY_ISO = `${FUTURE_EXPIRY_YMD}T00:00:00.000Z`;
const [FE_Y, FE_M, FE_D] = FUTURE_EXPIRY_YMD.split("-");
const FUTURE_EXPIRY_DE = `${FE_D}.${FE_M}.${FE_Y}`;
const src = readFileSync(
resolve(
process.cwd(),
@@ -135,13 +146,13 @@ describe("<InventorySection> — item-row dates", () => {
...BASE,
state: "ACTIVE",
firstUseAt: null,
printedExpiry: "2027-06-01T00:00:00.000Z",
printedExpiry: FUTURE_EXPIRY_ISO,
},
]);
const html = render(
<InventorySection medicationId="med-1" unitsPerDose={1} />,
);
expect(html).toContain("Haltbar bis 01.06.2027");
expect(html).toContain(`Haltbar bis ${FUTURE_EXPIRY_DE}`);
});
it("separates the two dates when the row carries both", () => {
@@ -149,14 +160,14 @@ describe("<InventorySection> — item-row dates", () => {
{
...BASE,
firstUseAt: "2026-06-12T00:00:00.000Z",
printedExpiry: "2027-06-01T00:00:00.000Z",
printedExpiry: FUTURE_EXPIRY_ISO,
},
]);
const html = render(
<InventorySection medicationId="med-1" unitsPerDose={1} />,
);
expect(html).toContain("Geöffnet 12.06.2026");
expect(html).toContain("Haltbar bis 01.06.2027");
expect(html).toContain(`Haltbar bis ${FUTURE_EXPIRY_DE}`);
expect(html).toContain("<span>·</span>");
});
@@ -174,7 +185,7 @@ describe("<AdjustInventoryDialog> — editable dates", () => {
const item: Row = {
...BASE,
firstUseAt: "2026-06-12T00:00:00.000Z",
printedExpiry: "2027-06-01T00:00:00.000Z",
printedExpiry: FUTURE_EXPIRY_ISO,
};
function renderDialog(over: Partial<Row> = {}): string {
@@ -191,7 +202,7 @@ describe("<AdjustInventoryDialog> — editable dates", () => {
const html = renderDialog();
expect(html).toContain('id="inventory-edit-expiry"');
expect(html).toContain("Aufgedrucktes Haltbarkeitsdatum");
expect(html).toContain('value="2027-06-01"');
expect(html).toContain(`value="${FUTURE_EXPIRY_YMD}"`);
});
it("offers an opening-date field prefilled from the item", () => {
@@ -205,7 +216,7 @@ describe("<AdjustInventoryDialog> — editable dates", () => {
const html = renderDialog({ firstUseAt: null, printedExpiry: null });
expect(html).toContain('id="inventory-edit-expiry"');
expect(html).toContain('id="inventory-edit-opened"');
expect(html).not.toContain("2027-06-01");
expect(html).not.toContain(FUTURE_EXPIRY_YMD);
expect(html).not.toContain("2026-06-12");
});