diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 099cb89b..66598661 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -1581,13 +1581,7 @@ export function MasterSearchHeader({ onBlur={(event) => { const nextFocusedElement = event.relatedTarget; if (nextFocusedElement instanceof Node && event.currentTarget.contains(nextFocusedElement)) return; - // Defer dismiss so a pointer activation on a menuitem can land before - // unmount; keyboard leave (Tab/Shift+Tab) still closes on the next frame. - const menuRoot = event.currentTarget; - window.requestAnimationFrame(() => { - if (menuRoot.contains(document.activeElement)) return; - setModeMenuOpen(false); - }); + setModeMenuOpen(false); }} className={cn("relative z-[60] min-w-0", isWorkflowHeader ? "justify-self-start" : "justify-self-center")} > diff --git a/src/components/forms/forms-home-page.tsx b/src/components/forms/forms-home-page.tsx index 3b01a409..b53a98b1 100644 --- a/src/components/forms/forms-home-page.tsx +++ b/src/components/forms/forms-home-page.tsx @@ -6,7 +6,6 @@ import { FileQuestion, FileText, Loader2, - Route, Search, ShieldAlert, ShieldCheck, @@ -30,22 +29,22 @@ import { countVerifiedRegistryRecords, useRegistryRecords } from "@/lib/use-regi const taskCards: ModeHomeAction[] = [ { title: "Find a form", - description: "Number, pathway, clock, keyword.", + description: "Title, purpose, or workflow detail.", icon: Search, href: appModeHomeHref("forms", { focus: true }), }, { title: "Readiness checks", - description: "Maker, clock, copies, source.", + description: "Review status, source, and local confirmation.", icon: ClipboardCheck, href: `/forms/${defaultFormSlug() ?? ""}`, }, { - title: "Browse pathways", - description: "Before, current, parallel, after.", - icon: Route, + title: "Check source status", + description: "Find records that still need local confirmation.", + icon: ShieldAlert, href: appModeHomeHref("forms", { - query: "forms pathway before current parallel after", + query: "local confirmation required", focus: true, run: true, }), @@ -91,9 +90,7 @@ export function FormsHomePage() { ) : registry.status === "error" ? ( { - for (const criterion of service.criteria ?? []) { - if (criterion.tone === "meet") total.meets += 1; - if (criterion.tone === "caution") total.cautions += 1; - if (criterion.tone === "reject") total.rejects += 1; - } - const confidence = service.verification?.confidence ?? "Unknown"; - if (confidence === "High") total.high += 1; - else if (confidence === "Medium") total.medium += 1; - else if (confidence === "Low") total.low += 1; - else total.unknown += 1; - if (service.verification?.locallyVerified || (service.source?.status ?? "").toLowerCase().includes("source")) { - total.verified += 1; - } - if ((service.source?.status ?? "").toLowerCase().includes("confirmation")) total.localConfirmation += 1; - return total; - }, - { meets: 0, cautions: 0, rejects: 0, high: 0, medium: 0, low: 0, unknown: 0, verified: 0, localConfirmation: 0 }, - ); -} - function Stepper() { return (
@@ -220,7 +198,7 @@ function ServiceCard({ @@ -339,7 +342,8 @@ function RightRail({ + Edit via result controls
{rows.map(([label, count, Icon, color]) => ( @@ -380,27 +379,61 @@ function RightRail({ ))}
+ {checklistExpanded ? ( +
+ {selected.map((service) => ( +
+

{service.title}

+
    + {(service.criteria ?? []).map((criterion) => ( +
  • • {criterion.label}
  • + ))} +
+
+ ))} +
+ ) : null}

Source confidence

-
- - - - +
+ {confidenceTotal > 0 ? ( + <> + {counts.high ? : null} + {counts.medium ? ( + + ) : null} + {counts.low ? : null} + {counts.unknown ? ( + + ) : null} + + ) : null}
@@ -424,15 +457,68 @@ function RightRail({ {counts.unknown}
+ {confidenceExpanded ? ( +
+ {matches.slice(0, 8).map((service) => ( +
+ {service.title} + + {service.verification?.confidence ?? "Unknown"} + +
+ ))} + {matches.length > 8 ? ( +

+{matches.length - 8} more results

+ ) : null} +
+ ) : null}
+ {comparisonExpanded ? ( +
+ {selected.map((service) => ( +
+
+

{service.title}

+ + Open + +
+
+ {[ + ["Contact", text(service.primaryContact?.value)], + ["Eligibility", text(service.eligibility, "Eligibility pending")], + ["Cost", text(service.cost, "Cost pending")], + ["Source", text(service.source?.status, "Source pending")], + ["Confidence", text(service.verification?.confidence, "Unknown")], + ].map(([label, value]) => ( +
+
{label}
+
{value}
+
+ ))} +
+
+ ))} +
+ ) : null} ); } diff --git a/tests/ui-accessibility.spec.ts b/tests/ui-accessibility.spec.ts index e18ffd1c..6be58fad 100644 --- a/tests/ui-accessibility.spec.ts +++ b/tests/ui-accessibility.spec.ts @@ -48,7 +48,7 @@ async function expectNoPageHorizontalOverflow(page: Page) { } async function expectDashboardUsable(page: Page) { - await expect(page.getByRole("heading", { level: 1, name: "Clinical Guide" })).toHaveCount(1); + await expect(page.getByRole("heading", { level: 1, name: "Clinical KB" })).toHaveCount(1); await expect(page.getByRole("heading", { name: "Answer" })).toBeVisible(); await expect(page.locator('[aria-label^="Search indexed guidelines by question or keyword"]:visible')).toBeVisible(); await expect(page.getByRole("button", { name: "Open answer options" })).toBeVisible(); diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 6f997165..841b848a 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -19,7 +19,7 @@ const dashboardViewports = [ { name: "laptop", width: 1280, height: 900 }, { name: "mobile-landscape", width: 667, height: 375 }, ] as const; -const uiAssertionTimeoutMs = 5_000; +const uiAssertionTimeoutMs = 30_000; const demoAnswerThreadOwnerId = "local-demo-session"; const demoAnswerThreadStorageKey = `${answerThreadStorageKey}:${demoAnswerThreadOwnerId}`; const demoRecentQueryStorageKey = `${recentQueryStorageKey}:${demoAnswerThreadOwnerId}`; @@ -867,7 +867,7 @@ test.describe("Clinical KB UI smoke coverage", () => { await gotoApp(page, "/"); await waitForDemoDashboardReady(page); - await expect(page.getByRole("heading", { level: 1, name: "Clinical Guide" })).toHaveCount(1); + await expect(page.getByRole("heading", { level: 1, name: "Clinical KB" })).toHaveCount(1); await expect(page.getByRole("heading", { name: "Answer" })).toBeVisible(); await expect(visibleQuestionInput(page)).toBeVisible(); await expect(page.getByRole("button", { name: "Generate source-backed answer" })).toHaveText(/^\s*Ask\s*$/); @@ -1262,7 +1262,7 @@ test.describe("Clinical KB UI smoke coverage", () => { await expect(page.getByRole("button", { name: "Generate source-backed answer" })).toBeEnabled(); await expect(page.getByTestId("answer-grounding-chip")).toHaveCount(0); expect(answerRequests).toEqual([]); - await expect(page.getByRole("heading", { level: 1, name: "Clinical Guide" })).toBeVisible(); + await expect(page.getByRole("heading", { level: 1, name: "Clinical KB" })).toBeVisible(); await expectDomIntegrity(page, { mobileNav: true, mobileFabReady: false }); await expectNoPageHorizontalOverflow(page); }); @@ -2400,11 +2400,16 @@ test.describe("Clinical KB UI smoke coverage", () => { test("dashboard favourites mode param redirects to the standalone favourites route", async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); await mockDemoApi(page); + const redirectMeasureErrors: string[] = []; + page.on("pageerror", (error) => { + if (error.message.includes("cannot have a negative time stamp")) redirectMeasureErrors.push(error.message); + }); await gotoApp(page, "/?mode=favourites&q=lithium%20set&focus=1"); await expect(page).toHaveURL(/\/favourites\?q=lithium\+set&focus=1$/); await expect(page.getByTestId("favourites-hub")).toBeVisible(); await expect(page.getByRole("heading", { name: "Favourites command library" })).toBeVisible(); + expect(redirectMeasureErrors).toEqual([]); }); test("dashboard differentials mode param redirects to the standalone differentials route", async ({ page }) => { diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 8f05b1f3..2ed5c540 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -882,7 +882,7 @@ test.describe("Clinical KB tools launcher", () => { await expectNoPageHorizontalOverflow(page); }); - test("forms mode shows source-backed form records in search results", async ({ page }) => { + test("forms mode shows registry-backed form records without unsupported pathway claims", async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); await mockAnswerDashboardApi(page); await gotoLauncher(page, "/forms?q=transport%20forms&focus=1&run=1"); @@ -902,6 +902,9 @@ test.describe("Clinical KB tools launcher", () => { await expect( page.getByTestId("form-search-result-transport-crisis-form").getByLabel("Open Transport order"), ).toHaveAttribute("href", "/forms/transport-crisis-form"); + await expect(page.getByRole("button", { name: "Refine" })).toHaveCount(0); + await expect(page.getByText(/Evidence 278|Pathways 12|Tasks 8|Source verified|Aligned to MHA 2014/)).toHaveCount(0); + await expect(page.getByText(/PSOLIS Transport|View full pathway/)).toHaveCount(0); await expect(page.getByTestId("service-search-results")).toHaveCount(0); await expectNoPageHorizontalOverflow(page); }); @@ -1002,6 +1005,7 @@ test.describe("Clinical KB tools launcher", () => { await expect(page.getByTestId("form-search-mobile-results")).toBeVisible(); await expect(page.getByTestId("form-search-mobile-result-transport-crisis-form")).toContainText("Transport order"); + await expect(page.getByText(/PSOLIS Transport|View full pathway|Source verified/)).toHaveCount(0); await expect(visibleGlobalSearchInput(page)).toHaveValue("transport"); await expectNoPageHorizontalOverflow(page); }); @@ -1014,6 +1018,19 @@ test.describe("Clinical KB tools launcher", () => { const dock = page.locator("form.answer-footer-search-dock"); await expect(dock).toBeVisible(); await expect(dock).not.toHaveAttribute("data-scroll-hidden", "true"); + const transition = await dock.evaluate((node) => { + const style = window.getComputedStyle(node); + const durationMs = Math.max( + ...style.transitionDuration.split(",").map((value) => { + const normalized = value.trim(); + const duration = Number.parseFloat(normalized); + return normalized.endsWith("ms") ? duration : duration * 1000; + }), + ); + return { durationMs, property: style.transitionProperty }; + }); + expect(transition.property).toMatch(/transform|all/); + expect(transition.durationMs).toBeGreaterThanOrEqual(100); // focus=1 leaves the composer focused; hide-on-scroll stays off while it has focus. const input = visibleGlobalSearchInput(page).first(); @@ -1603,6 +1620,7 @@ test.describe("Clinical KB service detail page", () => { { name: "desktop", width: 1280, height: 900 }, ] as const) { test(`13YARN service detail is usable at ${viewport.name}`, async ({ page }) => { + await mockAnswerDashboardApi(page); await page.setViewportSize({ width: viewport.width, height: viewport.height }); await gotoLauncher(page, "/services/13yarn"); @@ -1623,7 +1641,40 @@ test.describe("Clinical KB service detail page", () => { }); } + test("long mobile service details clear the bottom search dock at the scroll endpoint", async ({ page }) => { + await mockAnswerDashboardApi(page); + await page.setViewportSize({ width: 390, height: 820 }); + await gotoLauncher(page, "/services/city-east-community-mental-health-service"); + + const servicePage = page.getByTestId("service-detail-page"); + const footer = servicePage.getByText("Information accuracy may vary. Confirm locally before use."); + const scrollport = page.locator("#main-content"); + await expect(servicePage).toBeVisible(); + await scrollport.evaluate((element) => element.scrollTo({ top: element.scrollHeight, behavior: "instant" })); + + await expect + .poll(() => scrollport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)) + .toBeLessThanOrEqual(1); + + const clearance = await footer.evaluate((element) => { + const scrollElement = document.querySelector("#main-content"); + const dock = document.querySelector( + "form.answer-footer-search-dock, form.answer-footer-search-edge", + ); + if (!scrollElement || !dock) return null; + return { + footerBottom: element.getBoundingClientRect().bottom, + scrollBottom: scrollElement.getBoundingClientRect().bottom, + dockHeight: dock.getBoundingClientRect().height, + }; + }); + + expect(clearance).not.toBeNull(); + expect(clearance!.footerBottom).toBeLessThanOrEqual(clearance!.scrollBottom - clearance!.dockHeight - 8); + }); + test("service navigator action uses the shared global search route", async ({ page }) => { + await mockAnswerDashboardApi(page); await page.setViewportSize({ width: 1280, height: 900 }); await gotoLauncher(page, "/services/13yarn"); @@ -1634,6 +1685,7 @@ test.describe("Clinical KB service detail page", () => { }); test("service detail actions save, copy, and back from direct entry", async ({ page }) => { + await mockAnswerDashboardApi(page); await page.setViewportSize({ width: 1280, height: 900 }); await gotoLauncher(page, "/services/13yarn");