diff --git a/src/app/differentials/diagnoses/[slug]/error.tsx b/src/app/differentials/diagnoses/[slug]/error.tsx new file mode 100644 index 0000000000..517e6de4f4 --- /dev/null +++ b/src/app/differentials/diagnoses/[slug]/error.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useEffect } from "react"; +import { AlertTriangle, RefreshCw } from "lucide-react"; +import { primaryControl } from "@/components/ui-primitives"; +import { cn } from "@/components/ui-primitives"; + +export default function ErrorBoundary({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + useEffect(() => { + console.error("Unhandled runtime error captured in differentials diagnoses segment:", error); + }, [error]); + + return ( +
+
+
+ +
+ +

+ Failed to load differential details +

+ +

+ An unexpected error occurred while fetching the differential diagnosis details. +

+ + {error.digest && ( +
+ Digest: {error.digest} +
+ )} + +
+ +
+
+
+ ); +} diff --git a/src/app/differentials/diagnoses/[slug]/loading.tsx b/src/app/differentials/diagnoses/[slug]/loading.tsx new file mode 100644 index 0000000000..59334e70b5 --- /dev/null +++ b/src/app/differentials/diagnoses/[slug]/loading.tsx @@ -0,0 +1,5 @@ +import { ModeHomeRouteLoading } from "@/components/mode-home-page-skeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/differentials/presentations/[slug]/error.tsx b/src/app/differentials/presentations/[slug]/error.tsx new file mode 100644 index 0000000000..132d9587bc --- /dev/null +++ b/src/app/differentials/presentations/[slug]/error.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useEffect } from "react"; +import { AlertTriangle, RefreshCw } from "lucide-react"; +import { primaryControl } from "@/components/ui-primitives"; +import { cn } from "@/components/ui-primitives"; + +export default function ErrorBoundary({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + useEffect(() => { + console.error("Unhandled runtime error captured in differentials presentations segment:", error); + }, [error]); + + return ( +
+
+
+ +
+ +

+ Failed to load presentation workflow +

+ +

+ An unexpected error occurred while fetching the differential presentation workflow details. +

+ + {error.digest && ( +
+ Digest: {error.digest} +
+ )} + +
+ +
+
+
+ ); +} diff --git a/src/app/differentials/presentations/[slug]/loading.tsx b/src/app/differentials/presentations/[slug]/loading.tsx new file mode 100644 index 0000000000..59334e70b5 --- /dev/null +++ b/src/app/differentials/presentations/[slug]/loading.tsx @@ -0,0 +1,5 @@ +import { ModeHomeRouteLoading } from "@/components/mode-home-page-skeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/medications/[slug]/error.tsx b/src/app/medications/[slug]/error.tsx new file mode 100644 index 0000000000..57623ac02a --- /dev/null +++ b/src/app/medications/[slug]/error.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useEffect } from "react"; +import { AlertTriangle, RefreshCw } from "lucide-react"; +import { primaryControl } from "@/components/ui-primitives"; +import { cn } from "@/components/ui-primitives"; + +export default function ErrorBoundary({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + useEffect(() => { + console.error("Unhandled runtime error captured in medications segment:", error); + }, [error]); + + return ( +
+
+
+ +
+ +

+ Failed to load medication details +

+ +

+ An unexpected error occurred while fetching the prescribing details. +

+ + {error.digest && ( +
+ Digest: {error.digest} +
+ )} + +
+ +
+
+
+ ); +} diff --git a/src/app/medications/[slug]/loading.tsx b/src/app/medications/[slug]/loading.tsx new file mode 100644 index 0000000000..59334e70b5 --- /dev/null +++ b/src/app/medications/[slug]/loading.tsx @@ -0,0 +1,5 @@ +import { ModeHomeRouteLoading } from "@/components/mode-home-page-skeleton"; + +export default function Loading() { + return ; +} diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 84684daeec..2545d2b249 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -2154,16 +2154,13 @@ export function ClinicalDashboard({ useEffect(() => { const previous = prevAuthStatusRef.current; prevAuthStatusRef.current = authStatus; - if ( - (authStatus === "signed_out" || authStatus === "expired") && - (previous === "authenticated" || previous === "loading") - ) { + if ((authStatus === "signed_out" || authStatus === "expired") && previous === "authenticated") { resetAnswerThread(); setAnswer(null); setSources([]); latestAnswerTurnRef.current = null; } - }, [authStatus]); + }, [authStatus, resetAnswerThread]); const supabaseEnvStatus = setupChecks.find((check) => check.id === "env")?.status; const browserAuthUnavailableDemoFallback = !auth.isConfigured && supabaseEnvStatus !== "ready"; const localNoAuthMode = isLocalNoAuthMode(); diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 5b2a68c3ac..3d2bae636a 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -1,5 +1,6 @@ import type { Route } from "playwright-core"; import { expect, test, type Locator, type Page } from "playwright/test"; +import { answerThreadStorageKey } from "../src/lib/answer-thread-storage"; import { demoAnswer, demoDocuments, getDemoDocument, getDemoDocumentPayload } from "../src/lib/demo-data"; import { deriveGovernanceFromSections } from "../src/lib/medication-records"; import { getMedicationRecord, loadMedicationSnapshot } from "../src/lib/medication-snapshot"; @@ -551,6 +552,23 @@ async function waitForDemoDashboardReady(page: Page) { await expect(page.getByRole("button", { name: "Open answer options" })).toBeVisible({ timeout: 30000 }); } +async function waitForPersistedAnswerThread(page: Page, minPriorTurns = 1) { + await expect + .poll(async () => + page.evaluate((storageKey) => { + try { + const raw = window.localStorage.getItem(storageKey); + if (!raw) return 0; + const parsed = JSON.parse(raw) as { priorTurns?: unknown[] }; + return Array.isArray(parsed.priorTurns) ? parsed.priorTurns.length : 0; + } catch { + return 0; + } + }, answerThreadStorageKey), + ) + .toBeGreaterThanOrEqual(minPriorTurns); +} + async function openGuide(page: Page) { const viewport = page.viewportSize(); const dialog = page.getByRole("dialog", { name: "Clinical KB guide" }); @@ -1243,9 +1261,12 @@ test.describe("Clinical KB UI smoke coverage", () => { await expect(page).toHaveURL(/\?mode=answer&q=what\+about\+renal\+impairment\%3F&run=1/); await expectNoPageHorizontalOverflow(page); + await waitForPersistedAnswerThread(page, 1); await page.reload(); await waitForDemoDashboardReady(page); - await expect(page.getByTestId("user-question-bubble")).toHaveCount(2, { timeout: uiAssertionTimeoutMs }); + await expect(async () => { + await expect(page.getByTestId("user-question-bubble")).toHaveCount(2); + }).toPass({ timeout: 15_000 }); await expect(page.getByTestId("user-question-bubble").first()).toContainText(firstQuestion); await expect(page.getByTestId("user-question-bubble").nth(1)).toContainText(followUp); await expect(page.locator('[data-dashboard-stage="answer-thread-turn"][data-collapsed="true"]')).toHaveCount(1); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index b110efc708..ef9823b7d5 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -97,9 +97,12 @@ async function commandSurfaceOpensAbovePill(page: Page, hintPattern: RegExp) { { timeout: 10_000 }, ); await input.click(); - await input.press("ArrowDown"); + await expect(async () => { + await input.press("ArrowDown"); + await expect(page.getByText(hintPattern)).toBeVisible(); + await expect(page.getByRole("listbox").first()).toBeVisible(); + }).toPass({ timeout: 15_000 }); - await expect(page.getByText(hintPattern)).toBeVisible(); const listbox = page.getByRole("listbox").first(); await expect(listbox).toBeVisible(); @@ -407,6 +410,8 @@ test.describe("Clinical KB applications launcher", () => { await page.setViewportSize({ width: 390, height: 820 }); await gotoLauncher(page, "/services?q=13YARN&focus=1&run=1"); await expect(page.getByRole("button", { name: "Mode Services" })).toBeVisible(); + await expect(visibleGlobalSearchInput(page).first()).toBeVisible(); + await page.waitForLoadState("networkidle", { timeout: 15_000 }).catch(() => undefined); await commandSurfaceOpensAbovePill(page, /Searching services/i); await expectNoPageHorizontalOverflow(page); });