diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d5120a4..c758d3ce3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ - **MCP search cursors keep lab results stable across pages.** Distinct analyte names are ordered before offset pagination, preventing repeated or skipped lab entries when a client follows `nextCursor`. +- **Opening the medication wizard no longer risks clearing entered fields.** + The one-shot URL flag is removed without starting a second page navigation, + so slower browsers keep the active wizard instance and its form state. No migrations. No breaking changes. diff --git a/e2e/medications-wizard-daily.spec.ts b/e2e/medications-wizard-daily.spec.ts index 5bdb3855a..e6d773dd4 100644 --- a/e2e/medications-wizard-daily.spec.ts +++ b/e2e/medications-wizard-daily.spec.ts @@ -20,8 +20,9 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup"; /** * v1.5.4 — Medication wizard end-to-end — daily cadence. * - * Walks the 7-step daily path through the modal dialog. The dialog - * mounts when `/medications/new` redirects to `/medications?new=1`. + * Walks the 7-step daily path through the modal dialog. The shared helper + * opens `/medications?new=1` directly so cadence coverage is independent of + * the legacy `/medications/new` redirect. * The DE label surface (the maintainer's primary locale) is the assertion * surface — "Schritt N von 7" tracks the visible counter. */ diff --git a/e2e/medications-wizard-helpers.ts b/e2e/medications-wizard-helpers.ts index c2c236e17..1eba526d0 100644 --- a/e2e/medications-wizard-helpers.ts +++ b/e2e/medications-wizard-helpers.ts @@ -120,8 +120,9 @@ export async function stubDashboardAnalytics(page: Page): Promise { } /** - * Open the wizard dialog by hitting `/medications/new` (which - * redirects to `/medications?new=1` and opens the create dialog). + * Open the wizard directly on `/medications?new=1`. The redirect from the + * legacy `/medications/new` route is a separate concern; these cadence specs + * exercise the wizard without racing that server navigation. */ export async function openCreateWizard(page: Page): Promise { // Pin the German locale for the wizard flow. The app default locale is @@ -141,7 +142,11 @@ export async function openCreateWizard(page: Page): Promise { : "http://localhost:3000", }, ]); - await page.goto("/medications/new", { waitUntil: "domcontentloaded" }); + await page.goto("/medications?new=1", { waitUntil: "load" }); + await page.waitForURL( + (url) => url.pathname === "/medications" && !url.searchParams.has("new"), + { timeout: 10_000 }, + ); await expect( page.locator('[data-slot="medication-wizard-dialog"]'), ).toBeVisible({ timeout: 10_000 }); diff --git a/e2e/medications-wizard-oneshot.spec.ts b/e2e/medications-wizard-oneshot.spec.ts index e341811ba..d851f9a4a 100644 --- a/e2e/medications-wizard-oneshot.spec.ts +++ b/e2e/medications-wizard-oneshot.spec.ts @@ -33,6 +33,29 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup"; 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); diff --git a/src/app/medications/page-client.tsx b/src/app/medications/page-client.tsx index 63d2542a0..9e9483bca 100644 --- a/src/app/medications/page-client.tsx +++ b/src/app/medications/page-client.tsx @@ -198,12 +198,27 @@ export default function MedicationsPageClient() { const { layout, isLayoutLoading } = useMedicationListLayout(isAuthenticated); useEffect(() => { - if (shouldOpenFromUrl) { - // Drop the query param so a refresh after closing the dialog - // doesn't keep reopening it. - router.replace("/medications"); - } - }, [shouldOpenFromUrl, router]); + if (!shouldOpenFromUrl) return; + + // Child effects may run before the App Router installs its native-history + // integration, while the responsive sheet also settles its first client + // layout. Wait until the following painted frame so replaceState preserves + // Next's internals and synchronizes useSearchParams without starting a + // router navigation that can remount the open wizard and discard fields. + let cleanupFrame: number | undefined; + const layoutFrame = window.requestAnimationFrame(() => { + cleanupFrame = window.requestAnimationFrame(() => { + window.history.replaceState(null, "", "/medications"); + }); + }); + + return () => { + window.cancelAnimationFrame(layoutFrame); + if (cleanupFrame !== undefined) { + window.cancelAnimationFrame(cleanupFrame); + } + }; + }, [shouldOpenFromUrl]); const { data: medications,