diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 9afa8556f..e5953645c 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -734,6 +734,7 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-24 | cursor/search-performance-review-4ee9 (PR #1134) | 9311d01212fe42bd41ffb22a83bfa51f1a4d19f2 | Run PR re-sync sweep | Re-check: CONFLICTING on use-differential-catalog.ts (+ related). Not cheap; merge aborted, no push. | merge origin/main and/or conflict re-check only; no provider-backed checks run | | 2026-07-24 | cursor/comprehensive-repo-review-ledger-d9a1 (PR #1150) | 60a3c3a83a31e65ec2759540629687e7113e2489 | Run PR sweep: CI fix + threads + drift | Before: CONFLICTING, CI green, 0 threads. After: merged origin/main cleanly (ledger auto-merge); pushed 60a3c3a83. Threads: none. Residual: CI re-running. | merge origin/main only; no provider-backed checks run | | 2026-07-24 | cursor/search-interactive-perf-af54 (PR #1138) | ff4b293d95f922e70ebf5ee9b0c156c41a8bff3b | Run PR sweep: CI fix + threads + drift | Before: CONFLICTING, CI green, 0 threads. RAG impact: no retrieval behaviour change — PR is client deferred-search/UI only (no src/lib/rag/**). After: merged origin/main; conflict resolved in src/components/ui/sheet.tsx by keeping main restoreTimersRef/unmountingRef focus-restore fix; pushed ff4b293d9. Threads: none. | merge origin/main only; no provider-backed checks run | +| 2026-07-24 | cursor/information-page-structure-2a5d (PR #1148) | 97511d69256b97de4f4e654ff6c12f3742f795f4 | Run PR sweep: CI fix + threads + drift | Before: behind main. After: merged origin/main cleanly (no conflicts). Unresolved review threads left as non-P0/P1. CI not waited. | merge origin/main only; thread scan read-only; no provider-backed checks run | | 2026-07-24 | codex/audit-remediation-final (PR #1158) | 4bfaf2a77a5c8cc0e6c48ee27a72a2faad203dd3 | Run PR sweep: CI fix + threads + drift | Before: behind main by 64. After: merged origin/main cleanly (no conflicts). Threads: non-P0/P1 left open. CI not waited. | merge origin/main only; thread scan read-only; no provider-backed checks run | | 2026-07-24 | implement-audit-viewport-fixes (PR #1140) | 47ebd3d20184875d80bf192144b614ec58d48e08 | Run PR sweep: CI fix + threads + drift | Before: already contained origin/main. After: ledger-only record. Threads: non-P0/P1 left open. CI not waited. | merge origin/main only; thread scan read-only; no provider-backed checks run | | 2026-07-24 | codex/hydration-fixes (PR #1131) | 6c093e927d7b4f7261fb78160d85bdc407853001 | Run PR sweep: CI fix + threads + drift | Before: already contained origin/main. After: ledger-only record. Threads: non-P0/P1 left open. CI not waited. | merge origin/main only; thread scan read-only; no provider-backed checks run | @@ -753,6 +754,7 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-24 | codex/query-ribbon-search-headings (PR #1166) | 37cfa5553ccb784ee5e9f47ded1ad69914c053ed | Run PR babysit: CI/threads/drift | Final HEAD after merge origin/main + ledger bookkeeping. 0 unresolved threads; no Bugbot actionable findings; required CI re-running on this SHA. | merge origin/main; no provider-backed checks run. | | 2026-07-24 | codex/query-ribbon-search-headings (PR #1166) | 9eac2e252bcc5c548aa919b69faeb79b9ff7d2cf | Run PR babysit: CI/threads/drift | Merged origin/main; Codex ledger-SHA P2 dispositioned+resolved (append-only supersede already in 9eac2e252). 0 unresolved threads. | merge origin/main; thread resolve only; no provider-backed checks run. | | 2026-07-24 | remediate-audit-system-issues (PR #1160) | bdf530fc8c6faaa4491c510396b47872fc39bf25 | Run PR sweep: CI fix + threads + drift | second re-merge after main moved to 2e68888f3 during first push; clean ort merge (ledger + layout.tsx); taskkill /T retained; sitemap prettier retained | merge only; no provider-backed checks run | +| 2026-07-24 | cursor/information-page-structure-2a5d (PR #1148) | 230770a53f9bfb53ba69abc81d6e8491e021f8a9 | Run PR re-sync sweep | Before: CONFLICTING. After: merged origin/main clean. | merge origin/main and/or conflict re-check only; no provider-backed checks run | | 2026-07-24 | codex/audit-remediation-final (PR #1158) | 78fab6be0c43cf5e92361315399d393ee7742f2e | Run PR re-sync sweep | Before: CONFLICTING. After: merged origin/main clean (no RAG conflict markers). NOTE: PR still intentionally adds deterministic broad_summary queryClass shortcut in src/lib/rag/rag.ts (+17) — RAG impact behaviour change, not dropped during merge. | merge origin/main and/or conflict re-check only; no provider-backed checks run | | 2026-07-24 | implement-audit-viewport-fixes (PR #1140) | a541b75c0e49f84125fc7e5d114cd9fc32d1a694 | Run PR re-sync sweep | Before: CONFLICTING. After: merged origin/main clean. | merge origin/main and/or conflict re-check only; no provider-backed checks run | | 2026-07-24 | codex/hydration-fixes (PR #1131) | a29b0d778b542932972aa6035ee115b91e49025a | Run PR re-sync sweep | Before: CONFLICTING + Static PR FAIL (suppressHydrationWarning on skip link). After: merged origin/main; removed illegal suppressHydrationWarning from skip-to-content anchor; theme fix already on PR head b4b5f21b9. CI re-running. | merge origin/main and/or conflict re-check only; no provider-backed checks run | diff --git a/docs/codebase-index.md b/docs/codebase-index.md index b31eb7f95..4a8f9afe2 100644 --- a/docs/codebase-index.md +++ b/docs/codebase-index.md @@ -303,7 +303,15 @@ sequenceDiagram One shared composer (`master-search-header.tsx`) serves every mode. Placement: +<<<<<<< HEAD + +- **Mode homes** (`/services`, `/forms`, `/favourites`, `/differentials`, `/formulation`, `/tools`, and dashboard homes): inline in the hero via the `mode-home-composer-slot` portal, on phone and tablet+ alike. (`/applications` is a redirect to `/tools`, not a composer surface.) +- **Information (detail) pages**: catalogue/record routes under each mode (`/services/[slug]`, `/forms/[slug]`, `/medications/[slug]`, `/specifiers/[slug]`, `/formulation/[slug]`, `/factsheets/[slug]`, `/dsm/diagnoses/[slug]`, …). Route detection: `src/lib/information-pages.ts` (`isInformationPage`). Shared outer chrome: `src/components/information-page-shell.tsx` (`InformationPageShell`, breadcrumbs, optional footer). Specifier/formulation mode shells re-export that primitive. Intentional opt-outs: document viewer, therapy-compass CSS workspace, differentials presentation workflow. + \======= - **Mode homes** (`/services`, `/forms`, `/favourites`, `/differentials`, `/dsm`, `/specifiers`, `/formulation`, `/factsheets`, `/therapy-compass`, `/tools`, and dashboard homes): inline in the hero via the `mode-home-composer-slot` portal, on phone and tablet+ alike. (`/applications` is a redirect to `/tools`, not a composer surface.) + +> > > > > > > origin/main + - **Result and detail views**: fixed bottom dock on phone (compact variant on submitted searches), sticky top from `sm` up. - **Results routing**: standalone routes own their submitted searches via `?q=…&run=1` (`/services` → `ServicesNavigatorPage`, `/forms` → `FormsSearchResultsPage`, `/differentials` → `DifferentialsHome` results view, `/formulation` → local mechanism results, `/favourites` filters the command library in place). Answer, Documents, and Prescribing submitted searches render inside `ClinicalDashboard` — intentional, since they need retrieval/answer state. `/?mode=favourites` redirects to `/favourites`; `/?mode=differentials` redirects to `/differentials`; `/?mode=dsm` redirects to `/dsm`; `/?mode=specifiers` redirects to `/specifiers`; `/?mode=formulation` redirects to `/formulation`. - **Intentionally composer-free routes**: `/differentials/presentations/*` (comparison workflow owns its chrome), `/documents/[id]` viewer (has its own in-document ask composer), `/documents/source/*` (document flow owns mobile chrome). Do not re-flag these in search-consistency audits. diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 4a1461679..675e13be1 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -48,6 +48,7 @@ import { } from "@/lib/app-modes"; import { isLocalNoAuthMode, resolveClientDemoMode } from "@/lib/client-env"; import { documentsSearchHref } from "@/lib/document-flow-routes"; +import { isInformationPage } from "@/lib/information-pages"; import { differentialsMobileCompareAddonSlotId, modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer"; import { readSearchNavigationContext, type SearchNavigationOptions } from "@/lib/search-navigation-context"; import { shouldRenderClinicalDashboard, shouldRenderDashboardSearch } from "@/lib/search-route-ownership"; @@ -164,65 +165,6 @@ function GlobalSearchShellClient(props: GlobalSearchShellProps) { ); } -function isInformationPage(pathname: string): boolean { - // Services detail: /services/[slug] - if (pathname.startsWith("/services/") && pathname !== "/services") return true; - - // Forms detail: /forms/[slug] - if (pathname.startsWith("/forms/") && pathname !== "/forms") return true; - - // Medications detail: /medications/[slug] - if (pathname.startsWith("/medications/") && pathname !== "/medications") return true; - - // Psychiatric specifier detail: /specifiers/[slug] - if ( - pathname.startsWith("/specifiers/") && - pathname !== "/specifiers" && - pathname !== "/specifiers/builder" && - pathname !== "/specifiers/compare" && - pathname !== "/specifiers/map" - ) - return true; - - // Clinical formulation detail: /formulation/[slug] - if ( - pathname.startsWith("/formulation/") && - pathname !== "/formulation" && - pathname !== "/formulation/builder" && - pathname !== "/formulation/compare" && - pathname !== "/formulation/map" - ) - return true; - - // Factsheets detail: /factsheets/[slug] - if (pathname.startsWith("/factsheets/") && pathname !== "/factsheets" && pathname !== "/factsheets/search") - return true; - - // Therapy compass detail: /therapy-compass/[slug]/brief or /therapy-compass/[slug]/sheet - if ( - pathname.startsWith("/therapy-compass/") && - pathname !== "/therapy-compass" && - pathname !== "/therapy-compass/compare" && - pathname !== "/therapy-compass/pathways" && - pathname !== "/therapy-compass/recommend" && - pathname !== "/therapy-compass/review" && - pathname !== "/therapy-compass/search" - ) - return true; - - // Differential diagnosis detail: /differentials/diagnoses/[slug] or /differentials/presentations/[slug] - if (pathname.startsWith("/differentials/diagnoses/") || pathname.startsWith("/differentials/presentations/")) - return true; - - // DSM-5 Diagnosis detail: /dsm/diagnoses/[slug] or /dsm/diagnoses/[slug]/differentials or /dsm/compare - if (pathname.startsWith("/dsm/diagnoses/")) return true; - - // Document detail: /documents/[id] (excluding /documents/search) - if (pathname.startsWith("/documents/") && pathname !== "/documents/search") return true; - - return false; -} - function isToolDetailWithFooterSearch(pathname: string): boolean { return ( (pathname.startsWith("/services/") && pathname !== "/services") || diff --git a/src/components/clinical-dashboard/medication-record-page.tsx b/src/components/clinical-dashboard/medication-record-page.tsx index 42d973756..6c9ca862c 100644 --- a/src/components/clinical-dashboard/medication-record-page.tsx +++ b/src/components/clinical-dashboard/medication-record-page.tsx @@ -4,7 +4,6 @@ import { Activity, Ban, TriangleAlert, - ArrowLeft, BadgeCheck, BookOpen, CalendarDays, @@ -19,7 +18,6 @@ import { Timer, type LucideIcon, } from "lucide-react"; -import Link from "next/link"; import { useMemo, useRef, useState, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent } from "react"; import { BadgeCluster } from "@/components/clinical-dashboard/clinical-badge"; @@ -46,12 +44,17 @@ import { cn, EmptyState, LoadingPanel, - pageContainer, toneDanger, toneInfo, toneSuccess, toneWarning, } from "@/components/ui-primitives"; +import { + InformationPageBreadcrumbs, + InformationPageFooter, + InformationPageShell, +} from "@/components/information-page-shell"; +import { appModeHomeHref } from "@/lib/app-modes"; const sectionIcons: Record = { dose: CalendarDays, @@ -387,7 +390,7 @@ function MedicationRecordDetail({ const activeSections = sectionsByTab[activeTab]; return ( -
+
@@ -496,27 +499,19 @@ export function MedicationRecordPage({ const governance = data?.governance ?? (error ? undefined : fallbackGovernance); return ( -
-
- -
-
+ + +
{record ? ( ) : loading ? ( -
- -
+ ) : (
@@ -526,9 +521,9 @@ export function MedicationRecordPage({
)}
-
+ Clinical KB provides evidence summaries, not medical advice. Verify clinical decisions. -
-
+ + ); } diff --git a/src/components/dsm/dsm-diagnosis-page.tsx b/src/components/dsm/dsm-diagnosis-page.tsx index f812f8219..b54c2c774 100644 --- a/src/components/dsm/dsm-diagnosis-page.tsx +++ b/src/components/dsm/dsm-diagnosis-page.tsx @@ -12,6 +12,7 @@ import { } from "lucide-react"; import { DsmHeaderActions, DsmPageHeader } from "@/components/dsm/dsm-page-header"; +import { InformationPageShell } from "@/components/information-page-shell"; import { cn, codeText, metadataPill, pageContainer } from "@/components/ui-primitives"; import { dsmCriteria, resolveDsmDifferential, type DsmDiagnosis, type DsmLabeledText } from "@/lib/dsm"; @@ -36,7 +37,7 @@ export function DsmDiagnosisPage({ diagnosis }: { diagnosis: DsmDiagnosis }) { const compareHref = `/dsm/compare?ids=${encodeURIComponent(diagnosis.slug)}`; return ( -
+ Supplied local catalogue
- + ); } diff --git a/src/components/dsm/dsm-page-header.tsx b/src/components/dsm/dsm-page-header.tsx index 384c01bac..983e54482 100644 --- a/src/components/dsm/dsm-page-header.tsx +++ b/src/components/dsm/dsm-page-header.tsx @@ -1,7 +1,8 @@ import Link from "next/link"; -import { ArrowLeft, BookOpenCheck, GitCompareArrows, Search } from "lucide-react"; +import { BookOpenCheck, GitCompareArrows, Search } from "lucide-react"; import type { ReactNode } from "react"; +import { InformationPageBreadcrumbs } from "@/components/information-page-shell"; import { cn, codeText, metadataPill, pageContainer } from "@/components/ui-primitives"; export function DsmPageHeader({ @@ -26,14 +27,8 @@ export function DsmPageHeader({
- - - DSM-5 Diagnosis home - -

+ +

{eyebrow}

@@ -41,11 +36,11 @@ export function DsmPageHeader({
-

+

{title}

{description ? ( -

+

{description}

) : null} diff --git a/src/components/factsheets/factsheet-detail-page.tsx b/src/components/factsheets/factsheet-detail-page.tsx index f688bb1c4..79db149d1 100644 --- a/src/components/factsheets/factsheet-detail-page.tsx +++ b/src/components/factsheets/factsheet-detail-page.tsx @@ -2,7 +2,6 @@ import Link from "next/link"; import { - ArrowLeft, ArrowUpRight, Bookmark, Check, @@ -28,6 +27,7 @@ import { type Factsheet, } from "@/components/factsheets/factsheets-data"; import { factsheetGlyph } from "@/components/factsheets/factsheets-icons"; +import { InformationPageBreadcrumbs, InformationPageShell } from "@/components/information-page-shell"; import { cn, toneDanger, toneWarning } from "@/components/ui-primitives"; import { readSavedRegistrySlugs, @@ -105,17 +105,14 @@ export function FactsheetDetailPage({ factsheet }: { factsheet: Factsheet }) { return ( <> -
+ {/* action bar */}
- -
- +
); } diff --git a/src/components/specifiers/specifier-ui.tsx b/src/components/specifiers/specifier-ui.tsx index 101d842d8..d7b67c121 100644 --- a/src/components/specifiers/specifier-ui.tsx +++ b/src/components/specifiers/specifier-ui.tsx @@ -1,18 +1,9 @@ import Link from "next/link"; import type { ComponentType, CSSProperties, ReactNode } from "react"; -import { - ArrowLeft, - ArrowRight, - CheckCircle2, - ChevronRight, - ChevronsUpDown, - Info, - Minus, - ShieldAlert, - Tags, -} from "lucide-react"; +import { ArrowRight, CheckCircle2, ChevronsUpDown, Info, Minus, ShieldAlert, Tags } from "lucide-react"; -import { cn, eyebrowText, pageContainer } from "@/components/ui-primitives"; +import { InformationPageBreadcrumbs, InformationPageShell } from "@/components/information-page-shell"; +import { cn, eyebrowText } from "@/components/ui-primitives"; import type { SpecifierFamily, SpecifierRecord } from "@/lib/specifiers"; import { specifierFamilies } from "@/lib/specifiers"; import type { SpecifierSourceStatus } from "@/lib/specifiers-search-index"; @@ -21,43 +12,11 @@ export const specifierCard = "rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] shadow-[var(--shadow-inset)]"; export function SpecifierPageShell({ children, className }: { children: ReactNode; className?: string }) { - return ( -
-
{children}
-
- ); + return {children}; } export function SpecifierBreadcrumbs({ current }: { current?: string }) { - return ( - - ); + return ; } export function SpecifierSubnav({ active }: { active: "search" | "builder" | "compare" | "map" }) { diff --git a/src/lib/information-pages.ts b/src/lib/information-pages.ts new file mode 100644 index 000000000..0f045d04e --- /dev/null +++ b/src/lib/information-pages.ts @@ -0,0 +1,78 @@ +/** + * Information pages are per-mode detail / record surfaces (not mode homes, builders, + * compare/map tools, or search results). The global search shell uses this to suppress + * the floating composer on read-focused routes. + * + * Basic chrome for these pages lives in `src/components/information-page-shell.tsx`. + * Intentional opt-outs from that shell (different product chrome): document viewer, + * therapy-compass CSS workspace, differentials presentation workflow. + */ + +export type InformationPageMode = + | "services" + | "forms" + | "prescribing" + | "specifiers" + | "formulation" + | "factsheets" + | "therapy-compass" + | "differentials" + | "dsm" + | "documents"; + +const TOOL_SUFFIXES = new Set(["builder", "compare", "map"]); + +function isSlugDetail(pathname: string, home: string, extraExcluded: string[] = []): boolean { + if (!pathname.startsWith(`${home}/`) || pathname === home) return false; + const rest = pathname.slice(home.length + 1); + if (!rest || rest.includes("/")) return false; + if (extraExcluded.includes(rest) || TOOL_SUFFIXES.has(rest)) return false; + return true; +} + +/** + * True when `pathname` is a mode information (detail/record) page. + * Keep in sync with adoption notes on `InformationPageShell`. + */ +export function isInformationPage(pathname: string): boolean { + if (isSlugDetail(pathname, "/services")) return true; + if (isSlugDetail(pathname, "/forms")) return true; + if (isSlugDetail(pathname, "/medications")) return true; + if (isSlugDetail(pathname, "/specifiers")) return true; + if (isSlugDetail(pathname, "/formulation")) return true; + if (isSlugDetail(pathname, "/factsheets", ["search"])) return true; + + // Therapy compass detail: /therapy-compass/[slug]/brief or /sheet (and bare slug if present) + if ( + pathname.startsWith("/therapy-compass/") && + pathname !== "/therapy-compass" && + pathname !== "/therapy-compass/compare" && + pathname !== "/therapy-compass/pathways" && + pathname !== "/therapy-compass/recommend" && + pathname !== "/therapy-compass/review" && + pathname !== "/therapy-compass/search" + ) { + return true; + } + + if (pathname.startsWith("/differentials/diagnoses/") || pathname.startsWith("/differentials/presentations/")) { + return true; + } + + if (pathname.startsWith("/dsm/diagnoses/")) return true; + + if (pathname.startsWith("/documents/") && pathname !== "/documents/search") return true; + + return false; +} + +/** Modes that use the shared `InformationPageShell` for outer chrome. */ +export const informationPageShellModes = [ + "services", + "forms", + "prescribing", + "specifiers", + "formulation", + "factsheets", + "dsm", +] as const satisfies readonly InformationPageMode[]; diff --git a/tests/clinical-dashboard-merge-artifacts.test.ts b/tests/clinical-dashboard-merge-artifacts.test.ts index 6283d1908..18d705d99 100644 --- a/tests/clinical-dashboard-merge-artifacts.test.ts +++ b/tests/clinical-dashboard-merge-artifacts.test.ts @@ -50,6 +50,10 @@ const formulationUiSource = readFileSync( resolve(process.cwd(), "src/components/formulation/formulation-ui.tsx"), "utf8", ); +const informationPageShellSource = readFileSync( + resolve(process.cwd(), "src/components/information-page-shell.tsx"), + "utf8", +); const favouritesLibrarySource = readFileSync( resolve(process.cwd(), "src/components/clinical-dashboard/favourites-command-library-page.tsx"), "utf8", @@ -172,13 +176,16 @@ describe("ClinicalDashboard merge-artifact guards", () => { expect(differentialsHomeSource).not.toContain("pb-[calc(12.5rem+env(safe-area-inset-bottom))]"); expect(applicationsLauncherSource).not.toContain("pb-[calc(12rem+env(safe-area-inset-bottom))]"); expect(serviceDetailSource).not.toContain("pb-[calc(5.5rem+env(safe-area-inset-bottom))]"); - expect(serviceDetailSource).toContain("max-sm:min-h-0"); + expect(serviceDetailSource).toContain("InformationPageShell"); expect(formDetailSource).not.toContain("pb-[calc(2rem+env(safe-area-inset-bottom))]"); - expect(formDetailSource).toContain("max-sm:min-h-0"); + expect(formDetailSource).toContain("InformationPageShell"); expect(specifierUiSource).not.toContain("pb-[calc(7rem+env(safe-area-inset-bottom))]"); - expect(specifierUiSource).toContain("max-sm:min-h-0"); + expect(specifierUiSource).toContain("InformationPageShell"); expect(formulationUiSource).not.toContain("pb-[calc(7rem+env(safe-area-inset-bottom))]"); - expect(formulationUiSource).toContain("max-sm:min-h-0"); + expect(formulationUiSource).toContain("InformationPageShell"); + // Phone min-height contract lives on the shared information-page shell so + // catalogue detail pages do not each reintroduce a 100dvh pad under the dock. + expect(informationPageShellSource).toContain("max-sm:min-h-0"); expect(favouritesLibrarySource).not.toContain("pb-[calc(6rem+env(safe-area-inset-bottom))]"); expect(globalStylesSource).toContain("--phone-dock-hidden-pad: 0rem"); }); diff --git a/tests/forms-back-navigation.dom.test.tsx b/tests/forms-back-navigation.dom.test.tsx index a668315c4..136cfbc64 100644 --- a/tests/forms-back-navigation.dom.test.tsx +++ b/tests/forms-back-navigation.dom.test.tsx @@ -1,18 +1,9 @@ -import { fireEvent, render, screen } from "@testing-library/react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; import { FormDetailPage } from "@/components/forms/form-detail-page"; import { formRecords } from "@/lib/forms"; -const router = vi.hoisted(() => ({ - back: vi.fn(), - push: vi.fn(), -})); - -vi.mock("next/navigation", () => ({ - useRouter: () => router, -})); - vi.mock("@/components/account-data-provider", () => ({ useAccountData: () => ({ isSaved: () => false, @@ -20,22 +11,10 @@ vi.mock("@/components/account-data-provider", () => ({ }), })); -beforeEach(() => { - router.back.mockReset(); - router.push.mockReset(); -}); - describe("Form detail back navigation", () => { - it("uses the canonical Forms route even when browser history has prior entries", () => { - window.history.pushState({}, "", "/unrelated-route"); - window.history.pushState({}, "", "/forms/transport-crisis-form"); - expect(window.history.length).toBeGreaterThan(1); - + it("links to the canonical Forms home with focus", () => { render(); - fireEvent.click(screen.getByRole("button", { name: "Back to forms" })); - - expect(router.push).toHaveBeenCalledOnce(); - expect(router.push).toHaveBeenCalledWith("/forms?focus=1"); - expect(router.back).not.toHaveBeenCalled(); + const back = screen.getByRole("link", { name: /Forms/i }); + expect(back).toHaveAttribute("href", "/forms?focus=1"); }); }); diff --git a/tests/information-page-shell.test.tsx b/tests/information-page-shell.test.tsx new file mode 100644 index 000000000..701e612fb --- /dev/null +++ b/tests/information-page-shell.test.tsx @@ -0,0 +1,56 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { InformationPageBreadcrumbs, InformationPageShell } from "@/components/information-page-shell"; +import { isInformationPage } from "@/lib/information-pages"; + +describe("isInformationPage", () => { + it("recognises catalogue detail routes per mode", () => { + expect(isInformationPage("/services/acuity")).toBe(true); + expect(isInformationPage("/forms/transport-crisis-form")).toBe(true); + expect(isInformationPage("/medications/lithium")).toBe(true); + expect(isInformationPage("/specifiers/with-anxious-distress")).toBe(true); + expect(isInformationPage("/formulation/avoidance")).toBe(true); + expect(isInformationPage("/factsheets/ssri-start")).toBe(true); + expect(isInformationPage("/dsm/diagnoses/mdd")).toBe(true); + expect(isInformationPage("/differentials/diagnoses/delirium")).toBe(true); + expect(isInformationPage("/documents/abc")).toBe(true); + }); + + it("excludes mode homes, builders, and search surfaces", () => { + expect(isInformationPage("/services")).toBe(false); + expect(isInformationPage("/forms")).toBe(false); + expect(isInformationPage("/specifiers/builder")).toBe(false); + expect(isInformationPage("/formulation/compare")).toBe(false); + expect(isInformationPage("/factsheets/search")).toBe(false); + expect(isInformationPage("/documents/search")).toBe(false); + expect(isInformationPage("/therapy-compass/search")).toBe(false); + }); +}); + +describe("InformationPageShell", () => { + it("renders a main landmark with the default container", () => { + render( + +

Body

+
, + ); + const main = screen.getByTestId("info-shell"); + expect(main.tagName).toBe("MAIN"); + expect(main.querySelector(".max-w-7xl")).not.toBeNull(); + expect(screen.getByText("Body")).toBeInTheDocument(); + }); + + it("exposes shared breadcrumbs as a link back to the mode home", () => { + render( + , + ); + const home = screen.getByRole("link", { name: /Forms/i }); + expect(home).toHaveAttribute("href", "/forms?focus=1"); + expect(screen.getByText("Transport")).toHaveAttribute("aria-current", "page"); + }); +}); diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 4195749c3..e9b83c715 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -2939,7 +2939,7 @@ test.describe("Clinical KB UI smoke coverage", () => { await acamprosateResult.click(); await expect(page).toHaveURL(/\/medications\/acamprosate$/, { timeout: 30_000 }); await expectSingleMedicationPage(page); - await expect(page.getByRole("link", { name: "Back to medication search" })).toBeVisible(); + await expect(page.getByRole("link", { name: "Medications" })).toBeVisible(); expect(parentNodeErrors).toEqual([]); }); @@ -2971,7 +2971,7 @@ test.describe("Clinical KB UI smoke coverage", () => { await acamprosateCard.click(); await expect(page).toHaveURL(/\/medications\/acamprosate$/, { timeout: 30_000 }); - const backLink = page.getByRole("link", { name: "Back", exact: true }); + const backLink = page.getByRole("link", { name: "Medications", exact: true }); await expect(backLink).toBeVisible(); await expectMinTouchTarget(backLink); await backLink.click(); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index a6595ddb6..9a9f1b752 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -1989,7 +1989,7 @@ test.describe("Clinical KB service detail page", () => { const copyContactButton = servicePage.getByRole("button", { name: "Copy contact" }).last(); await expect(servicePage).toBeVisible(); await expect(servicePage.getByRole("heading", { level: 1, name: "13YARN" })).toBeVisible(); - await expect(page.getByRole("button", { name: "Back to services" })).toBeVisible(); + await expect(servicePage.getByRole("link", { name: "Services" })).toBeVisible(); await expect(servicePage.getByRole("button", { name: "Save service" })).toBeVisible(); await expect(copyContactButton).toBeVisible(); await expect(servicePage.getByRole("link", { name: "Call" })).toHaveAttribute("href", "tel:139276"); @@ -2083,7 +2083,7 @@ test.describe("Clinical KB service detail page", () => { await servicePage.getByRole("button", { name: "Copy contact" }).last().click(); await expect(page.getByRole("status")).toContainText("Contact copied"); - await page.getByRole("button", { name: "Back to services" }).click(); + await servicePage.getByRole("link", { name: "Services" }).click(); await expect(page).toHaveURL(/\/services(?:\?|$)/); }); });