fix(medications): preserve wizard state during URL cleanup

This commit is contained in:
Marc-André Bombeck committed 2026-07-21 18:09:23 +02:00
1 parent 7f55083c3c
commit 25a56983b3
5 files changed
+58 -11

No files matched your search

+3
View File
@@ -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.
+3 -2
View File
@@ -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.
*/
+8 -3
View File
@@ -120,8 +120,9 @@ export async function stubDashboardAnalytics(page: Page): Promise<void> {
}
/**
* 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<void> {
// Pin the German locale for the wizard flow. The app default locale is
@@ -141,7 +142,11 @@ export async function openCreateWizard(page: Page): Promise<void> {
: "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 });
+23
View File
@@ -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);
+21 -6
View File
@@ -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,