mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
A spec that arms `page.route(...)` in a `beforeEach` and never drops the handler races its own teardown. The context closes, an async handler is still awaiting, its next call rejects with "Test ended.", and Playwright attributes that to the test that had just passed. With `retries: 2` plus `failOnFlakyTests`, which spec bites is a matter of timing. `e2e/setup/test.ts` re-exports Playwright's `test` extended with an auto fixture that unroutes every page and the context after each test, and clears the page event listeners a spec subscribes to; `expect` rides along so a spec needs one import. Auto means a spec author cannot forget it and does not have to know it exists. Every spec now takes `test` from there, and `e2e-route-cleanup-guard.test.ts` keeps the import boundary closed with an allowlist that carries its reason. The per-file `afterEach` on the cycle spec goes away — leaving both would read as not trusting the fixture. Not covered: a page inside a context a spec builds itself. Only `share-documents.spec.ts` does that, and it owns those contexts.
133 lines
4.3 KiB
TypeScript
133 lines
4.3 KiB
TypeScript
import { expect, test } from "./setup/test";
|
|
|
|
import {
|
|
STUB_MEDICATION_ID,
|
|
clickNext,
|
|
clickSave,
|
|
expectMedicationOnList,
|
|
expectRedirectToMedication,
|
|
expectStep,
|
|
fillStep1Name,
|
|
fillStep3Dose,
|
|
mockMedicationsApi,
|
|
openCreateWizard,
|
|
pickCadenceRow,
|
|
pickTreatmentRow,
|
|
stubDashboardAnalytics,
|
|
} from "./medications-wizard-helpers";
|
|
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
|
|
|
/**
|
|
* v1.5.4 — Medication wizard end-to-end — one-shot cadence.
|
|
*
|
|
* Picking "Einmalig" on Step 5 collapses the path onto the 5-step
|
|
* one-shot route ([1, 2, 3, 4, 8]). The wizard skips Steps 6 + 7
|
|
* because the dose date already lives in Step 4 (course window).
|
|
* Step 4's CourseWindowRow locks endsOn to startsOn for one-shot.
|
|
*
|
|
* The list-page "Nächste Einnahme:" chip is suppressed because the
|
|
* design-synthesis lifecycle deactivates one-shot medications once
|
|
* the single intake is logged. The spec only asserts the row name
|
|
* appears on the list.
|
|
*/
|
|
test.describe("medication wizard — one-shot", () => {
|
|
test.use({ storageState: STORAGE_STATE_PATH });
|
|
|
|
test("does not replay the one-shot URL flag after history traversal", async ({
|
|
page,
|
|
}) => {
|
|
await stubDashboardAnalytics(page);
|
|
await openCreateWizard(page);
|
|
|
|
await page.keyboard.press("Escape");
|
|
await expect(
|
|
page.locator('[data-slot="medication-wizard-dialog"]'),
|
|
).toHaveCount(0);
|
|
|
|
await page.locator('a[href="/"]:visible').first().click();
|
|
await page.waitForURL((url) => url.pathname === "/");
|
|
await page.goBack();
|
|
await page.waitForURL(
|
|
(url) => url.pathname === "/medications" && !url.searchParams.has("new"),
|
|
);
|
|
|
|
await expect(
|
|
page.locator('[data-slot="medication-wizard-dialog"]'),
|
|
).toHaveCount(0);
|
|
});
|
|
|
|
test("creates a single-dose medication end-to-end", async ({ page }) => {
|
|
test.slow();
|
|
await stubDashboardAnalytics(page);
|
|
const capture = mockMedicationsApi(page, {
|
|
id: STUB_MEDICATION_ID,
|
|
name: "Grippeimpfung",
|
|
dose: "0.5 ml",
|
|
rrule: null,
|
|
rollingIntervalDays: null,
|
|
oneShot: true,
|
|
});
|
|
|
|
await openCreateWizard(page);
|
|
|
|
await fillStep1Name(page, { name: "Grippeimpfung" });
|
|
await clickNext(page);
|
|
|
|
await expectStep(page, 2);
|
|
await pickTreatmentRow(page, "other");
|
|
await clickNext(page);
|
|
|
|
await expectStep(page, 3);
|
|
await fillStep3Dose(page, { amount: "0.5" });
|
|
await page.locator("#wizard-dose-unit").click();
|
|
await page.getByRole("option", { name: /^ml$/i }).click();
|
|
await clickNext(page);
|
|
|
|
// Step 4 — course window. Set startsOn to a deterministic date so
|
|
// the post body assertion below is stable.
|
|
await expectStep(page, 4);
|
|
// `DateField` rides a visible text overlay (the data-testid) that parses
|
|
// a typed ISO string back to the canonical value; the hidden native input
|
|
// (data-slot) carries the committed ISO. Type into the overlay, then blur
|
|
// so the parse commits.
|
|
await page.getByTestId("course-window-starts-field").fill("2026-10-15");
|
|
await page.getByTestId("course-window-starts-field").blur();
|
|
await expect(
|
|
page.locator('[data-slot="course-window-starts"]'),
|
|
).toHaveValue("2026-10-15");
|
|
await clickNext(page);
|
|
|
|
// Step 5 — pick Einmalig. The path compresses to 5 steps the
|
|
// moment the row is picked; the visible counter follows.
|
|
await expectStep(page, 5, 8);
|
|
await pickCadenceRow(page, "oneShot");
|
|
// After the pick the path is [1, 2, 3, 4, 8]; the cadence pick
|
|
// step (raw step 5) sits between slots 4 and 5 of 5, so the
|
|
// counter pins on slot 4 of 5 until Next.
|
|
await expectStep(page, 4, 5);
|
|
await clickNext(page);
|
|
|
|
await expectStep(page, 5, 5);
|
|
const summary = page.locator('[data-slot="wizard-summary"]');
|
|
await expect(summary).toContainText(/Einmaldosis/i);
|
|
// The summary renders the date through the locale-aware formatter
|
|
// (DE UI → "15.10.2026"), not the ISO value the input carries.
|
|
await expect(summary).toContainText(/15\.10\.2026/);
|
|
|
|
await clickSave(page);
|
|
|
|
await expectRedirectToMedication(page, STUB_MEDICATION_ID);
|
|
expect(capture.postBody).toMatchObject({
|
|
name: "Grippeimpfung",
|
|
oneShot: true,
|
|
startsOn: "2026-10-15",
|
|
endsOn: "2026-10-15",
|
|
});
|
|
|
|
await expectMedicationOnList(page, {
|
|
name: "Grippeimpfung",
|
|
withNextDueChip: false,
|
|
});
|
|
});
|
|
});
|