mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
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:
1 parent
97cb63a77f
commit
7611c82ae7
4 files changed
+86
-11
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user