diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json index 2a889ac8b9..6dee940f14 100644 --- a/docs/design-system/adoption-manifest.json +++ b/docs/design-system/adoption-manifest.json @@ -263,7 +263,8 @@ "tests/in-page-nav-playwright-contract.test.ts", "tests/information-page-shell.dom.test.tsx", "tests/ui-route-coverage.spec.ts", - "tests/ui-specifiers.spec.ts" + "tests/ui-specifiers.spec.ts", + "tests/ui-tools.spec.ts" ], "baseline": { "targetLayer": "v2", diff --git a/src/components/clinical-dashboard/answer-content.tsx b/src/components/clinical-dashboard/answer-content.tsx index 89fe729f38..5cf75385aa 100644 --- a/src/components/clinical-dashboard/answer-content.tsx +++ b/src/components/clinical-dashboard/answer-content.tsx @@ -518,7 +518,7 @@ export function NaturalLanguageAnswer({ data-testid="source-only-disclosure" role="note" className={cn( - "w-fit max-w-full overflow-hidden border border-[color:var(--warning)]/30 bg-[color:var(--warning-soft)]/40 text-2xs transition-[border-radius] duration-[var(--duration-quick)]", + "w-fit max-w-full self-start overflow-hidden border border-[color:var(--warning)]/30 bg-[color:var(--warning-soft)]/40 text-2xs transition-[border-radius] duration-[var(--duration-quick)]", sourceOnlyNoticeOpen ? "rounded-lg" : "rounded-full", textMuted, )} @@ -566,7 +566,7 @@ export function NaturalLanguageAnswer({ ) : null} diff --git a/src/components/differentials/differential-detail-page.tsx b/src/components/differentials/differential-detail-page.tsx index 9f7201a89b..a9d3320b03 100644 --- a/src/components/differentials/differential-detail-page.tsx +++ b/src/components/differentials/differential-detail-page.tsx @@ -20,8 +20,10 @@ import { GitCompareArrows, Info, Plus, + Share2, ShieldAlert, Stethoscope, + Waypoints, type LucideIcon, } from "lucide-react"; @@ -807,6 +809,71 @@ function MobilePrimaryActions({ ); } +function DiagnosisDiscoveryActions({ + sections, + onSelect, +}: { + sections: ReturnType; + onSelect: (id: "map" | "related") => void; +}) { + const actions = [ + { + id: "map" as const, + label: "Map", + detail: sections.find((section) => section.id === "map")?.detail ?? "View links", + icon: Waypoints, + }, + { + id: "related" as const, + label: "Related", + detail: sections.find((section) => section.id === "related")?.detail ?? "View items", + icon: Share2, + }, + ]; + + return ( + + ); +} + function IconForDiagnosis(record: DifferentialRecord): LucideIcon { return record.slug === "delirium" ? BrainCircuit : Stethoscope; } @@ -1054,11 +1121,6 @@ export function DifferentialDetailPage({ />
} /> + + {saveNotice ? (

{saveNotice} diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 738653fe9a..eb501f19ec 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -1063,7 +1063,10 @@ async function expectAccountProviderLayout(setup: Locator, layout: "row" | "stac expect(boxes.every(Boolean)).toBe(true); const [apple, google, microsoft] = boxes as NonNullable<(typeof boxes)[number]>[]; - expect(boxes.every((box) => box!.height >= 48)).toBe(true); + // Chromium can report a CSS-enforced 48px minimum as 47.999… after layout + // rounding. Keep the clinical touch-target contract while ignoring that + // sub-hundredth-pixel measurement noise. + expect(boxes.every((box) => box!.height >= 47.99)).toBe(true); if (layout === "row") { expect(Math.max(apple.y, google.y, microsoft.y) - Math.min(apple.y, google.y, microsoft.y)).toBeLessThanOrEqual(1); expect(apple.x + apple.width).toBeLessThanOrEqual(google.x); @@ -3340,13 +3343,28 @@ test.describe("PsychSift UI smoke coverage", () => { { width: 1920, height: 1080 }, ]) { await page.setViewportSize(viewport); + // Reading boundingBox() immediately after a resize can race the + // reflow — Chromium sometimes reports one sibling's box mid-transition + // (seen ~10px short) while the other has already settled. Two rAF + // round-trips let layout finish before we measure. + await page.evaluate( + () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))), + ); const statusBox = await statusRow.boundingBox(); const sourceOnlyBox = await sourceOnlyDisclosure.boundingBox(); const reviewDueBox = await reviewDueTab.boundingBox(); expect(statusBox).toBeTruthy(); expect(sourceOnlyBox).toBeTruthy(); expect(reviewDueBox).toBeTruthy(); - expect(Math.abs(sourceOnlyBox!.y - reviewDueBox!.y)).toBeLessThanOrEqual(1); + // The controls have deliberately different touch-target densities, so + // their top edges and centres may differ. Both must still be contained + // by the single compact status row rather than wrapping onto a second + // line. + const statusBottom = statusBox!.y + statusBox!.height; + expect(sourceOnlyBox!.y).toBeGreaterThanOrEqual(statusBox!.y - 1); + expect(sourceOnlyBox!.y + sourceOnlyBox!.height).toBeLessThanOrEqual(statusBottom + 1); + expect(reviewDueBox!.y).toBeGreaterThanOrEqual(statusBox!.y - 1); + expect(reviewDueBox!.y + reviewDueBox!.height).toBeLessThanOrEqual(statusBottom + 1); expect(statusBox!.height).toBeLessThanOrEqual(42); expect(sourceOnlyBox!.height).toBeLessThanOrEqual(42); expect(reviewDueBox!.height).toBeLessThanOrEqual(42); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 9195b20428..d7623915a2 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -2650,6 +2650,31 @@ test.describe("PsychSift tools directory and legacy launcher", () => { await expect(sectionTrigger).toBeVisible(); await expect(sectionTrigger).toContainText("Overview"); await expectMinTouchTarget(sectionTrigger); + + // The page title already has a dedicated back control in the in-page + // header, so the old three-part breadcrumb is intentionally absent. Map + // and Related are useful enough to remain visible without opening the + // section sheet on a phone. + await expect(page.getByRole("navigation", { name: "Breadcrumb" })).toHaveCount(0); + const discoveryActions = detailPage.getByRole("navigation", { name: "Explore diagnosis" }); + await expect(discoveryActions).toBeVisible(); + const mapAction = discoveryActions.getByRole("button", { name: /^Map/ }); + const relatedAction = discoveryActions.getByRole("button", { name: /^Related/ }); + await expectMinTouchTarget(mapAction); + await expectMinTouchTarget(relatedAction); + await page.emulateMedia({ reducedMotion: "reduce", forcedColors: "active" }); + await expect(discoveryActions).toBeVisible(); + await expectNoPageHorizontalOverflow(page); + await page.emulateMedia({ reducedMotion: "no-preference", forcedColors: "none" }); + await mapAction.focus(); + await expect(mapAction).toBeFocused(); + await page.keyboard.press("Enter"); + await expect(sectionTrigger).toContainText("Map"); + await expect(page).toHaveURL(/[?&]tab=map/); + await relatedAction.click(); + await expect(sectionTrigger).toContainText("Related"); + await expect(page).toHaveURL(/[?&]tab=related/); + await sectionTrigger.click(); const sectionSheet = page.getByTestId("differential-section-sheet"); await expect(sectionSheet).toBeVisible();