onSelectItem(item.id)}
className={cn(
- "relative h-14 cursor-pointer transition hover:bg-[color:var(--surface-subtle)]",
+ "relative h-14 transition hover:bg-[color:var(--surface-subtle)]",
selected &&
"bg-[color:var(--clinical-accent-soft)]/45 shadow-[inset_3px_0_0_var(--clinical-accent)]",
)}
@@ -659,7 +706,10 @@ function FavouritesTable({
type="button"
onClick={() => onSelectItem(item.id)}
aria-pressed={selected}
- className={cn("flex min-w-0 max-w-full items-center gap-2.5 rounded-md text-left", focusRing)}
+ className={cn(
+ "hidden min-w-0 max-w-full items-center gap-2.5 rounded-md text-left xl:flex",
+ focusRing,
+ )}
>
@@ -682,6 +732,17 @@ function FavouritesTable({
+
+
+ {item.title}
+
+
+ {item.description}
+
+
{item.type}
@@ -713,6 +774,7 @@ function FavouritesTable({
{tableRows.map((item) => (
-
+
))}
{tableRows.length === 0 ? (
@@ -776,12 +833,13 @@ function FavouritesTable({
function ItemWorkspace({ item, onClose }: { item: FavouriteItem; onClose: () => void }) {
const [activeTab, setActiveTab] = useState<"summary" | "evidence" | "notes">("summary");
+ const [copyStatus, setCopyStatus] = useState<"idle" | "copied" | "failed">("idle");
const Icon = item.icon;
const actionLabel = item.action === "Copy" ? "Open" : item.action;
return (
@@ -869,53 +927,34 @@ function ItemWorkspace({ item, onClose }: { item: FavouriteItem; onClose: () =>
{activeTab === "evidence" ? (
- Sources (3)
+ Evidence
-
- {sourceRecords.map((source, index) => (
-
-
- {index + 1}
-
-
- {source.title}
-
-
- {source.type}
-
+
+ {isSourceBacked(item) ? (
+
- ))}
+ ) : (
+
+ No linked evidence source is saved for this item.
+
+ )}
) : null}
{activeTab === "notes" ? (
-
-
- Personal note
-
-
-
- Edit
-
-
+
+ Personal note
+
-
- Useful for older patients with fluctuating eGFR. Check adherence section on page 4.
+
+ No personal note is saved for this item.
-
- Updated 11 May 2024
-
) : null}
@@ -927,14 +966,25 @@ function ItemWorkspace({ item, onClose }: { item: FavouriteItem; onClose: () =>
{
+ const copied = await copyFavouriteCitation(item);
+ setCopyStatus(copied ? "copied" : "failed");
+ }}
className={cn(
"inline-flex h-9 items-center justify-start gap-2 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] px-3 text-sm font-bold text-[color:var(--text)] hover:bg-[color:var(--surface-subtle)]",
focusRing,
)}
>
- Copy citation
+ {copyStatus === "copied" ? "Copied" : copyStatus === "failed" ? "Copy failed" : "Copy citation"}
+
+ {copyStatus === "copied"
+ ? `${item.title} citation copied`
+ : copyStatus === "failed"
+ ? "Unable to copy citation"
+ : ""}
+
);
}
-export function FavouritesCommandLibraryPage({ query = "" }: { query?: string }) {
+export function FavouritesCommandLibraryPage({ query = "", demoMode }: { query?: string; demoMode: boolean }) {
const router = useRouter();
const command = useSearchCommand();
const [navCollapsed, setNavCollapsed] = useFavouritesNavCollapsed();
const savedRegistryFavourites = useSavedRegistryFavourites();
const items = useMemo(
- () => [...prototypeFavouriteItems, ...savedRegistryFavourites].map(toCommandItem),
- [savedRegistryFavourites],
+ () => [...(demoMode ? prototypeFavouriteItems : []), ...savedRegistryFavourites].map(toCommandItem),
+ [demoMode, savedRegistryFavourites],
);
const sets = useMemo(() => buildFavouriteSets(items), [items]);
const [selectedTypeId, setSelectedTypeId] = useState("all");
@@ -1023,8 +1073,8 @@ export function FavouritesCommandLibraryPage({ query = "" }: { query?: string })
navCollapsed ? "lg:grid-cols-[5.25rem_minmax(0,1fr)]" : "lg:grid-cols-[17.5rem_minmax(0,1fr)]",
selectedItem &&
(navCollapsed
- ? "2xl:grid-cols-[5.25rem_minmax(0,1fr)_23rem]"
- : "2xl:grid-cols-[17.5rem_minmax(0,1fr)_23rem]"),
+ ? "xl:grid-cols-[5.25rem_minmax(0,1fr)_23rem]"
+ : "xl:grid-cols-[17.5rem_minmax(0,1fr)_23rem]"),
)}
>
setViewMode("all")}
/>
- {showContinueStrip && continueItem ? (
-
- ) : null}
+ {showContinueStrip && continueItem ? : null}
{query.trim() && scopedItems.length === 0 ? (
@@ -1119,7 +1167,9 @@ export function FavouritesCommandLibraryPage({ query = "" }: { query?: string })
- {selectedItem ?
setSelectedItemId(null)} /> : null}
+ {selectedItem ? (
+ setSelectedItemId(null)} />
+ ) : null}
);
diff --git a/src/components/clinical-dashboard/favourites-hub.tsx b/src/components/clinical-dashboard/favourites-hub.tsx
index 8b34efc70..e411c4931 100644
--- a/src/components/clinical-dashboard/favourites-hub.tsx
+++ b/src/components/clinical-dashboard/favourites-hub.tsx
@@ -1,18 +1,7 @@
"use client";
import Link from "next/link";
-import {
- ArrowUpDown,
- ChevronDown,
- Filter,
- Folder,
- FolderInput,
- Heart,
- Plus,
- Search,
- ShieldCheck,
- X,
-} from "lucide-react";
+import { ArrowUpDown, ChevronDown, Filter, Folder, Heart, Plus, Search, ShieldCheck, X } from "lucide-react";
import { useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react";
import { useDismissableLayer } from "@/components/use-dismissable-layer";
import { ModeHomeHero, ModeHomeVerificationFooter } from "@/components/mode-home-template";
@@ -40,20 +29,24 @@ function favouriteMatchesQuery(value: { title: string; meta?: string; set?: stri
export function FavouritesHub({
query,
onClearQuery,
- onAddFavourite,
desktopComposerSlotId,
+ demoMode,
headingLevel = 2,
}: {
query: string;
onClearQuery: () => void;
- onAddFavourite: () => void;
desktopComposerSlotId?: string;
+ demoMode: boolean;
headingLevel?: 1 | 2;
}) {
const savedRegistryFavourites = useSavedRegistryFavourites();
- const allFavouriteItems = useMemo(() => [...favouriteItems, ...savedRegistryFavourites], [savedRegistryFavourites]);
+ const allFavouriteItems = useMemo(
+ () => [...(demoMode ? favouriteItems : []), ...savedRegistryFavourites],
+ [demoMode, savedRegistryFavourites],
+ );
const allFavouriteSets = useMemo(() => {
- const savedSetTitles = new Set(favouriteSets.map((set) => set.title));
+ const prototypeSets = demoMode ? favouriteSets : [];
+ const savedSetTitles = new Set(prototypeSets.map((set) => set.title));
const dynamicSets: FavouriteSet[] = Array.from(new Set(savedRegistryFavourites.map((item) => item.set)))
.filter((title): title is string => Boolean(title) && !savedSetTitles.has(title))
.map((title) => ({
@@ -67,13 +60,13 @@ export function FavouritesHub({
keywords: title.toLowerCase(),
}));
return [
- ...favouriteSets.map((set) => ({
+ ...prototypeSets.map((set) => ({
...set,
count: allFavouriteItems.filter((item) => item.set === set.title).length,
})),
...dynamicSets,
].filter((set) => set.count > 0);
- }, [allFavouriteItems, savedRegistryFavourites]);
+ }, [allFavouriteItems, demoMode, savedRegistryFavourites]);
const [selectedTab, setSelectedTab] = useState("all");
const [tabMenuOpen, setTabMenuOpen] = useState(false);
const [selectedSetId, setSelectedSetId] = useState(null);
@@ -312,14 +305,23 @@ export function FavouritesHub({
) : null}
-
+
Recent
Add favourite
@@ -392,7 +394,7 @@ export function FavouritesHub({
{showItems
? visibleItems.map((item) => (
- setSelectedTab("sets")} />
+ setSelectedTab("sets")} />
))
: null}
@@ -443,8 +445,9 @@ export function FavouritesHub({
New set
@@ -458,7 +461,7 @@ export function FavouritesHub({
);
}
-function FavouriteItemRow({ item, onMoveToSet }: { item: FavouriteItem; onMoveToSet: () => void }) {
+function FavouriteItemRow({ item, onBrowseSets }: { item: FavouriteItem; onBrowseSets: () => void }) {
const Icon = item.icon;
return (
@@ -479,21 +482,18 @@ function FavouriteItemRow({ item, onMoveToSet }: { item: FavouriteItem; onMoveTo
{item.primaryAction}
-
-
- Move
+
+
+ Browse sets
-
{
- window.location.href = item.href;
- }}
+
-
+
);
}
diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx
index 979dc30a5..14e631659 100644
--- a/src/components/clinical-dashboard/global-search-shell.tsx
+++ b/src/components/clinical-dashboard/global-search-shell.tsx
@@ -314,11 +314,8 @@ function GlobalStandaloneSearchShellClient({
// Recent queries are owner-scoped session state (2026-07-13 audit, finding 4):
// the legacy unscoped localStorage value could resurface another account's
// clinical queries on a shared workstation, so it is deleted, never read.
- const recentQueriesOwnerId =
- auth.session?.user.id ??
- (!auth.isConfigured || process.env.NEXT_PUBLIC_DEMO_MODE === "true" || isLocalNoAuthMode()
- ? demoRecentQueryOwnerId
- : null);
+ const clientDemoMode = !auth.isConfigured || process.env.NEXT_PUBLIC_DEMO_MODE === "true" || isLocalNoAuthMode();
+ const recentQueriesOwnerId = auth.session?.user.id ?? (clientDemoMode ? demoRecentQueryOwnerId : null);
useEffect(() => {
clearLegacyRecentQueries();
@@ -512,6 +509,7 @@ function GlobalStandaloneSearchShellClient({
("up");
const [commandDropdownOpen, setCommandDropdownOpen] = useState(false);
const [commandListboxId, setCommandListboxId] = useState();
+ const [commandActiveItemId, setCommandActiveItemId] = useState(null);
const [modeMenuOpen, setModeMenuOpen] = useState(false);
const [usesScopeSheet, setUsesScopeSheet] = useState(false);
const [usesPhoneSearchLayout, setUsesPhoneSearchLayout] = useState(false);
@@ -1267,6 +1270,7 @@ export function MasterSearchHeader({
/>
) : null}
onCommandScopesChange?.(scopes)}
onListboxIdReady={setCommandListboxId}
+ onActiveItemIdChange={setCommandActiveItemId}
onFocusSearchInput={() => queryInputRef?.current?.focus()}
>
) => void;
onFocusSearchInput?: () => void;
onListboxIdReady?: (listboxId: string) => void;
+ onActiveItemIdChange?: (activeItemId: string | null) => void;
placement?: CommandSurfacePlacement;
children: ReactNode;
}) {
@@ -420,7 +424,10 @@ export function UniversalSearchCommandSurface({
contextMode: modeId,
});
const savedRegistryFavourites = useSavedRegistryFavourites();
- const allFavouriteItems = useMemo(() => [...favouriteItems, ...savedRegistryFavourites], [savedRegistryFavourites]);
+ const allFavouriteItems = useMemo(
+ () => [...(demoMode ? favouriteItems : []), ...savedRegistryFavourites],
+ [demoMode, savedRegistryFavourites],
+ );
const favouriteMatches = useMemo(
() => rankLocalFavourites(allFavouriteItems, trimmedQuery),
[allFavouriteItems, trimmedQuery],
@@ -929,6 +936,10 @@ export function UniversalSearchCommandSurface({
onListboxIdReady?.(listboxId);
}, [listboxId, onListboxIdReady]);
+ useEffect(() => {
+ onActiveItemIdChange?.(dropdownOpen ? activeItemId : null);
+ }, [activeItemId, dropdownOpen, onActiveItemIdChange]);
+
useEffect(() => {
if (!dropdownOpen) return;
diff --git a/src/components/clinical-dashboard/visual-evidence.tsx b/src/components/clinical-dashboard/visual-evidence.tsx
index 8f5deb79b..c6c48f9c4 100644
--- a/src/components/clinical-dashboard/visual-evidence.tsx
+++ b/src/components/clinical-dashboard/visual-evidence.tsx
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
-import { useState } from "react";
+import { type KeyboardEvent, useId, useRef, useState } from "react";
import {
CircleAlert,
BookOpen,
@@ -378,6 +378,8 @@ function EvidenceClaimsList({ rows, renderModel }: { rows: AnswerEvidenceMapRow[
const claimRows = claimRowsForEvidencePanel(rows, renderModel);
const directCount = claimRows.filter((row) => supportLabel(row.supportLevel) === "Direct").length;
const partialCount = claimRows.filter((row) => supportLabel(row.supportLevel) === "Partial").length;
+ const claimRowClassName =
+ "grid min-h-[76px] grid-cols-[auto_auto_minmax(0,1fr)_auto] items-center gap-3 border-b border-[color:var(--border)] px-3 py-3 text-left last:border-b-0";
if (!claimRows.length) {
return ;
@@ -409,29 +411,52 @@ function EvidenceClaimsList({ rows, renderModel }: { rows: AnswerEvidenceMapRow[
- {claimRows.map((row, index) => (
-
-
-
-
- {row.section}
-
- {row.detail || row.bestLinkedPassage || row.bestSourceLabel}
+ {claimRows.map((row, index) => {
+ const content = (
+ <>
+
+
+
+ {row.section}
+
+ {row.detail || row.bestLinkedPassage || row.bestSourceLabel}
+
-
-
-
- ))}
+ {row.href ? (
+
+ ) : (
+ Source unavailable
+ )}
+ >
+ );
+
+ if (!row.href) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return (
+
+ {content}
+
+ );
+ })}
);
@@ -495,9 +520,40 @@ export function MobileEvidenceSheetContent({
const order = evidenceTabOrder(answer, renderModel);
const [selectedTab, setSelectedTab] = useState
(() => initialTab ?? null);
const activeTab = selectedTab && order.includes(selectedTab) ? selectedTab : order[0];
- const panelIdFor = (tab: EvidenceTabName) => `mobile-evidence-panel-${tab.toLowerCase()}`;
+ const tabSetId = useId();
+ const tabRefs = useRef>>({});
+ const tabIdFor = (tab: EvidenceTabName) => `${tabSetId}-mobile-evidence-tab-${tab.toLowerCase()}`;
+ const panelIdFor = (tab: EvidenceTabName) => `${tabSetId}-mobile-evidence-panel-${tab.toLowerCase()}`;
const [added, setAdded] = useState(false);
const primarySourceHref = renderModel.primarySources[0]?.href;
+
+ function handleTabKeyDown(event: KeyboardEvent, tab: EvidenceTabName) {
+ const currentIndex = order.indexOf(tab);
+ let nextIndex: number;
+
+ switch (event.key) {
+ case "ArrowRight":
+ nextIndex = (currentIndex + 1) % order.length;
+ break;
+ case "ArrowLeft":
+ nextIndex = (currentIndex - 1 + order.length) % order.length;
+ break;
+ case "Home":
+ nextIndex = 0;
+ break;
+ case "End":
+ nextIndex = order.length - 1;
+ break;
+ default:
+ return;
+ }
+
+ event.preventDefault();
+ const nextTab = order[nextIndex];
+ setSelectedTab(nextTab);
+ tabRefs.current[nextTab]?.focus();
+ }
+
async function copyEvidence() {
if (renderModel.quoteCards.length) {
onCopyQuotes();
@@ -534,11 +590,16 @@ export function MobileEvidenceSheetContent({
key={tab}
type="button"
role="tab"
+ ref={(node) => {
+ tabRefs.current[tab] = node;
+ }}
aria-selected={selected}
aria-controls={panelIdFor(tab)}
- id={`mobile-evidence-tab-${tab.toLowerCase()}`}
+ id={tabIdFor(tab)}
+ tabIndex={selected ? 0 : -1}
data-testid={`mobile-evidence-tab-${tab.toLowerCase()}`}
onClick={() => setSelectedTab(tab)}
+ onKeyDown={(event) => handleTabKeyDown(event, tab)}
className={cn(
"inline-flex min-h-11 items-center gap-1.5 rounded-md border px-3 text-xs font-semibold transition",
selected
@@ -563,7 +624,7 @@ export function MobileEvidenceSheetContent({
key={tab}
id={panelIdFor(tab)}
role="tabpanel"
- aria-labelledby={`mobile-evidence-tab-${tab.toLowerCase()}`}
+ aria-labelledby={tabIdFor(tab)}
data-testid={`mobile-evidence-panel-${tab.toLowerCase()}`}
hidden={!selected}
className="min-h-[220px]"
diff --git a/src/components/differentials/differential-presentation-workflow-page.tsx b/src/components/differentials/differential-presentation-workflow-page.tsx
index 0bb0aa870..001b4f76a 100644
--- a/src/components/differentials/differential-presentation-workflow-page.tsx
+++ b/src/components/differentials/differential-presentation-workflow-page.tsx
@@ -192,7 +192,9 @@ function DesktopComparisonTable({
{workflow.selectedCount} of {workflow.totalCount} selected
@@ -200,7 +202,9 @@ function DesktopComparisonTable({
Edit columns
@@ -231,7 +235,7 @@ function DesktopComparisonTable({
>
Criteria
- Reorder
+ Comparison detail
{candidates.map((candidate) => (
@@ -322,13 +326,16 @@ function SelectedDifferentialsPanel({
candidates: CandidateView[];
}) {
const selectedCandidates = candidates.filter((candidate) => candidate.selected);
+ const remainingCount = Math.max(workflow.totalCount - selectedCandidates.length, 0);
return (
Selected differentials ({selectedCandidates.length} of {workflow.totalCount})
- +2
+ {remainingCount > 0 ? (
+ +{remainingCount} not selected
+ ) : null}
{selectedCandidates.map((candidate) => {
@@ -337,7 +344,9 @@ function SelectedDifferentialsPanel({
);
})}
- Long press to reorder. Tap to remove.
+
+ Open a selected differential to review its clinical record.
+
{selectedCandidates.map((candidate) => (
{candidate.record.title}
-
+
))}
@@ -532,7 +541,9 @@ function MobileComparison({
@@ -542,8 +553,10 @@ function MobileComparison({
diff --git a/src/components/differentials/differential-stream-page.tsx b/src/components/differentials/differential-stream-page.tsx
index 6d93e25c1..9219f8d37 100644
--- a/src/components/differentials/differential-stream-page.tsx
+++ b/src/components/differentials/differential-stream-page.tsx
@@ -59,7 +59,7 @@ export function DifferentialStreamPage({ stream, query = "" }: DifferentialStrea
Diagnosis-focused differential content
- {copy.cards.map((card) => (
+ {copy.cards.map((card, index) => (
{card.title}
{card.description}
- {card.examples.map((example) => (
-
+ {card.examples.map((example, index) => (
+
{example}
diff --git a/src/components/forms/form-detail-page.tsx b/src/components/forms/form-detail-page.tsx
index e2f321327..3799bc1fc 100644
--- a/src/components/forms/form-detail-page.tsx
+++ b/src/components/forms/form-detail-page.tsx
@@ -494,10 +494,6 @@ export function FormDetailPage({ form }: { form: FormRecord }) {
const relatedTags = useMemo(() => [...(form.tags ?? []), ...(form.catchments ?? [])].slice(0, 8), [form]);
function goBack() {
- if (window.history.length > 1) {
- router.back();
- return;
- }
router.push(appModeHomeHref("forms", { focus: true }));
}
diff --git a/src/components/route-error-boundary.tsx b/src/components/route-error-boundary.tsx
index 04f053b3a..1ef72a98d 100644
--- a/src/components/route-error-boundary.tsx
+++ b/src/components/route-error-boundary.tsx
@@ -41,7 +41,7 @@ export function RouteErrorBoundary({
const headingRef = useRef(null);
useEffect(() => {
console.error(logLabel, error);
- headingRef.current?.focus();
+ headingRef.current?.focus({ preventScroll: true });
}, [error, logLabel]);
return (
@@ -64,7 +64,9 @@ export function RouteErrorBoundary({
{title}
- {description}
+
+ {description}
+
{error.digest && (
diff --git a/src/components/services/services-navigator-page.tsx b/src/components/services/services-navigator-page.tsx
index 0459da34e..e664497bb 100644
--- a/src/components/services/services-navigator-page.tsx
+++ b/src/components/services/services-navigator-page.tsx
@@ -332,6 +332,7 @@ function RightRail({
type="button"
onClick={clearSelectedServices}
disabled={selected.length === 0}
+ title={selected.length === 0 ? "No selected services to clear" : "Clear selected services"}
>
Clear
diff --git a/src/components/therapy-compass/bindings.tsx b/src/components/therapy-compass/bindings.tsx
index 8b9491e17..eafee0256 100644
--- a/src/components/therapy-compass/bindings.tsx
+++ b/src/components/therapy-compass/bindings.tsx
@@ -229,7 +229,6 @@ export function TcProvider({ children }: { children: ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
-
const { data, loading, error, retry } = useTherapyData();
const therapies = useMemo(() => data?.therapies ?? [], [data]);
const pathways = useMemo(() => data?.pathways ?? [], [data]);
diff --git a/src/components/therapy-compass/nav.tsx b/src/components/therapy-compass/nav.tsx
index c5161d859..6ba1eb2fd 100644
--- a/src/components/therapy-compass/nav.tsx
+++ b/src/components/therapy-compass/nav.tsx
@@ -36,7 +36,7 @@ export function TherapyCompassNav() {
-
+
Therapy
diff --git a/src/components/therapy-compass/screens/brief-screen.tsx b/src/components/therapy-compass/screens/brief-screen.tsx
index e3ac9a866..3b2e1b3a8 100644
--- a/src/components/therapy-compass/screens/brief-screen.tsx
+++ b/src/components/therapy-compass/screens/brief-screen.tsx
@@ -75,7 +75,7 @@ export function BriefScreen() {
Fast scripts and steps drawn from each record’s delivery fields.
-
+
-
+
{/* decision summary */}
{/* tabs */}
-
+
Priorities
diff --git a/src/components/therapy-compass/screens/detail-screen.tsx b/src/components/therapy-compass/screens/detail-screen.tsx
index 04a479bfa..33788ada5 100644
--- a/src/components/therapy-compass/screens/detail-screen.tsx
+++ b/src/components/therapy-compass/screens/detail-screen.tsx
@@ -96,7 +96,7 @@ export function DetailScreen() {
{/* QUICK TILES */}
-
+
{/* RIGHT RAIL */}
-
+
At a glance
diff --git a/src/components/therapy-compass/screens/home-screen.tsx b/src/components/therapy-compass/screens/home-screen.tsx
index 4b67e09d3..27455d74c 100644
--- a/src/components/therapy-compass/screens/home-screen.tsx
+++ b/src/components/therapy-compass/screens/home-screen.tsx
@@ -51,12 +51,15 @@ export function HomeScreen() {
What therapy are you looking for?
- Search {b.therapies.length || "200+"} source-grounded therapy records by problem, symptom, skill or population
- — or jump into a clinical pathway.
+ Search {b.therapies.length} source-grounded therapy {b.therapies.length === 1 ? "record" : "records"} by
+ problem, symptom, skill or population — or jump into a clinical pathway.
-
+
@@ -98,7 +102,10 @@ export function HomeScreen() {
{/* quick tools */}
-
+
-
+
{pathways.map((p) => (
-
+
{featuredList.map((t) => (
-
+
Review queue
@@ -59,7 +59,7 @@ export function PathwaysScreen() {
)}
>
{/* pathway list */}
-
+
Pathways
{b.pathways.map((p) => {
@@ -227,7 +227,7 @@ export function PathwaysScreen() {
"Review source status, missing fields and patient-specific factors before clinical use."}
-
+
-
+
{/* BUILDER */}
svg { top: 19px; }
+ .tc-root .tc-home-search-input { padding-right: 16px !important; }
+ .tc-root .tc-home-search-button { position: static !important; width: 100%; }
+ .tc-root .tc-pathway-list { border-right: 0 !important; border-bottom: 1px solid var(--border); }
+ .tc-root .tc-compare-tabs {
+ gap: 18px !important;
+ overflow-x: auto;
+ overscroll-behavior-inline: contain;
+ scrollbar-width: thin;
+ }
+ .tc-root .tc-compare-tabs > * { flex: none; }
/* Phone: the comparison table must keep its column structure, so make the
table scroll horizontally instead of clipping the right-hand therapies. */
- .tc-root .tc-scroll-sm {
+ .tc-root .tc-scroll-sm,
+ .tc-root .tc-compare-table {
overflow-x: auto !important;
- overscroll-behavior-x: contain;
+ overscroll-behavior-inline: contain;
-webkit-overflow-scrolling: touch;
+ scrollbar-width: thin;
}
+ .tc-root .tc-paper { padding: 28px 22px !important; }
}
@media print {
@page { size: A4 portrait; margin: 14mm; }
diff --git a/tests/favourites-demo-boundary.test.ts b/tests/favourites-demo-boundary.test.ts
new file mode 100644
index 000000000..a77acd413
--- /dev/null
+++ b/tests/favourites-demo-boundary.test.ts
@@ -0,0 +1,41 @@
+import { readFileSync } from "node:fs";
+
+import { describe, expect, it } from "vitest";
+
+const routeSource = readFileSync(new URL("../src/app/favourites/page.tsx", import.meta.url), "utf8");
+const librarySource = readFileSync(
+ new URL("../src/components/clinical-dashboard/favourites-command-library-page.tsx", import.meta.url),
+ "utf8",
+);
+const hubSource = readFileSync(
+ new URL("../src/components/clinical-dashboard/favourites-hub.tsx", import.meta.url),
+ "utf8",
+);
+const dashboardSource = readFileSync(new URL("../src/components/ClinicalDashboard.tsx", import.meta.url), "utf8");
+const globalShellSource = readFileSync(
+ new URL("../src/components/clinical-dashboard/global-search-shell.tsx", import.meta.url),
+ "utf8",
+);
+const universalSearchSource = readFileSync(
+ new URL("../src/components/clinical-dashboard/universal-search-command-surface.tsx", import.meta.url),
+ "utf8",
+);
+
+describe("favourites demo-data boundary", () => {
+ it("passes trusted server demo state and never merges prototype favourites into live mode unconditionally", () => {
+ expect(routeSource).toContain("demoMode={isDemoMode()}");
+ expect(librarySource).toContain("...(demoMode ? prototypeFavouriteItems : [])");
+ expect(librarySource).not.toContain("[...prototypeFavouriteItems, ...savedRegistryFavourites]");
+ expect(dashboardSource).toContain("demoMode={clientDemoMode}");
+ expect(hubSource).toContain("...(demoMode ? favouriteItems : [])");
+ expect(hubSource).not.toContain("[...favouriteItems, ...savedRegistryFavourites]");
+ expect(hubSource).toContain('title="Additional sort options are coming soon"');
+ expect(hubSource).toContain('title="Adding favourites from this screen is coming soon"');
+ expect(hubSource).toContain('title="Creating favourite sets is coming soon"');
+ expect(hubSource).toContain("Browse sets");
+ expect(dashboardSource).not.toContain("Favourite creation is ready to connect.");
+ expect(globalShellSource).toContain("demoMode={clientDemoMode}");
+ expect(universalSearchSource).toContain("...(demoMode ? favouriteItems : [])");
+ expect(universalSearchSource).not.toContain("[...favouriteItems, ...savedRegistryFavourites]");
+ });
+});
diff --git a/tests/forms-back-navigation.dom.test.tsx b/tests/forms-back-navigation.dom.test.tsx
new file mode 100644
index 000000000..501df680f
--- /dev/null
+++ b/tests/forms-back-navigation.dom.test.tsx
@@ -0,0 +1,34 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { beforeEach, 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,
+}));
+
+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);
+
+ 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();
+ });
+});
diff --git a/tests/playwright-app-mode.ts b/tests/playwright-app-mode.ts
new file mode 100644
index 000000000..c67eb832c
--- /dev/null
+++ b/tests/playwright-app-mode.ts
@@ -0,0 +1,15 @@
+import { expect, type Page } from "playwright/test";
+
+export async function openAppModeMenu(page: Page, currentMode: string) {
+ const trigger = page.getByRole("button", { name: `Mode ${currentMode}` });
+ const menu = page.getByRole("menu", { name: "Choose app mode" });
+
+ await expect(trigger).toBeVisible();
+ await expect(async () => {
+ if (await menu.isVisible().catch(() => false)) return;
+ await trigger.click();
+ await expect(menu).toBeVisible({ timeout: 5_000 });
+ }).toPass({ timeout: 20_000 });
+
+ return menu;
+}
diff --git a/tests/playwright-project-isolation.test.ts b/tests/playwright-project-isolation.test.ts
new file mode 100644
index 000000000..64f81434d
--- /dev/null
+++ b/tests/playwright-project-isolation.test.ts
@@ -0,0 +1,17 @@
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { describe, expect, it } from "vitest";
+
+describe("Playwright production-project isolation", () => {
+ it("excludes advisory mockup cases from every required browser project", () => {
+ const source = readFileSync(resolve(process.cwd(), "playwright.config.ts"), "utf8");
+
+ for (const project of ["chromium", "firefox", "webkit"]) {
+ expect(source).toMatch(
+ new RegExp(`name: ["']${project}["'],\\s+testMatch: productionSpecPattern,\\s+grepInvert: mockupTag,`, "m"),
+ );
+ }
+
+ expect(source).toMatch(/name: ["']chromium-mockups["'],\s+testMatch: mockupSpecPattern,\s+grep: mockupTag,/m);
+ });
+});
diff --git a/tests/route-error-boundary-focus.dom.test.tsx b/tests/route-error-boundary-focus.dom.test.tsx
new file mode 100644
index 000000000..7b50ac074
--- /dev/null
+++ b/tests/route-error-boundary-focus.dom.test.tsx
@@ -0,0 +1,20 @@
+import { render, screen, waitFor } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import { RouteErrorBoundary } from "@/components/route-error-boundary";
+
+describe("RouteErrorBoundary focus management", () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it("moves focus to the recovery heading when an error replaces route content", async () => {
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
+
+ render(
undefined} />);
+
+ const heading = screen.getByRole("heading", { name: "Something went wrong" });
+ await waitFor(() => expect(document.activeElement).toBe(heading));
+ expect(screen.getByRole("alert")).toHaveTextContent("An unexpected error occurred");
+ });
+});
diff --git a/tests/route-error-boundary.test.ts b/tests/route-error-boundary.test.ts
index dd161c210..537951906 100644
--- a/tests/route-error-boundary.test.ts
+++ b/tests/route-error-boundary.test.ts
@@ -19,6 +19,8 @@ describe("RouteErrorBoundary", () => {
expect(markup).toContain("Something went wrong");
expect(markup).toContain("Try again");
+ expect(markup).toContain('tabindex="-1"');
+ expect(markup).toContain('role="alert"');
// Reload is opt-in; it is not shown by default.
expect(markup).not.toContain("Reload page");
});
@@ -66,8 +68,12 @@ describe("global-error boundary", () => {
expect(markup).toContain(" ({
+ usePathname: () => "/therapy-compass",
+ useSearchParams: () => new URLSearchParams(),
+ useRouter: () => ({ push: vi.fn(), replace: vi.fn(), prefetch: vi.fn() }),
+}));
+
+const therapy = {
+ slug: "test-therapy",
+ name: "Test therapy",
+ category: "Skills based",
+ tags: [],
+ aliases: [],
+ reviewStatus: "reviewed",
+};
+
+function response(body: unknown, ok = true, status = 200) {
+ return {
+ ok,
+ status,
+ json: vi.fn().mockResolvedValue(body),
+ };
+}
+
+afterEach(() => {
+ vi.unstubAllGlobals();
+});
+
+describe("Therapy Compass required data recovery", () => {
+ it("shows an honest load error, retries all required files, and recovers", async () => {
+ let failTherapies = true;
+ const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
+ const path = String(input);
+ if (path.endsWith("/therapies.json")) {
+ return failTherapies ? response(null, false, 503) : response([therapy]);
+ }
+ if (path.endsWith("/pathways.json")) return response([]);
+ if (path.endsWith("/reference.json")) return response({});
+ throw new Error(`Unexpected fetch: ${path}`);
+ });
+ vi.stubGlobal("fetch", fetchMock);
+
+ render(
+
+
+ ,
+ );
+
+ expect(await screen.findByRole("alert")).toHaveTextContent("Therapy Compass could not load");
+ expect(screen.queryByRole("heading", { name: "What therapy are you looking for?" })).not.toBeInTheDocument();
+
+ failTherapies = false;
+ fireEvent.click(screen.getByRole("button", { name: "Retry" }));
+
+ expect(await screen.findByRole("heading", { name: "What therapy are you looking for?" })).toBeInTheDocument();
+ expect(screen.getByText(/Search 1 source-grounded therapy record by/)).toBeInTheDocument();
+ expect(screen.queryByRole("alert")).not.toBeInTheDocument();
+ await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(6));
+ });
+});
diff --git a/tests/therapy-compass-responsive-contract.test.ts b/tests/therapy-compass-responsive-contract.test.ts
new file mode 100644
index 000000000..534b1b993
--- /dev/null
+++ b/tests/therapy-compass-responsive-contract.test.ts
@@ -0,0 +1,134 @@
+import { readFileSync } from "node:fs";
+
+import { describe, expect, it } from "vitest";
+
+const read = (relativePath: string) => readFileSync(new URL(`../${relativePath}`, import.meta.url), "utf8");
+const therapyPath = "src/components/therapy-compass";
+
+const stylesSource = read(`${therapyPath}/styles.tsx`);
+const therapyCardSource = read(`${therapyPath}/therapy-card.tsx`);
+const homeSource = read(`${therapyPath}/screens/home-screen.tsx`);
+const detailSource = read(`${therapyPath}/screens/detail-screen.tsx`);
+const compareSource = read(`${therapyPath}/screens/compare-screen.tsx`);
+const recommendSource = read(`${therapyPath}/screens/recommend-screen.tsx`);
+const pathwaysSource = read(`${therapyPath}/screens/pathways-screen.tsx`);
+const briefSource = read(`${therapyPath}/screens/brief-screen.tsx`);
+const sheetsSource = read(`${therapyPath}/screens/sheets-screen.tsx`);
+const otherSource = read(`${therapyPath}/screens/other-screen.tsx`);
+
+function classCount(source: string, className: string) {
+ return source.match(new RegExp(`className="[^"]*\\b${className}\\b[^"]*"`, "g"))?.length ?? 0;
+}
+
+function contrastRatio(firstHex: string, secondHex: string) {
+ const luminance = (hex: string) => {
+ const channels = hex
+ .slice(1)
+ .match(/.{2}/g)!
+ .map((channel) => Number.parseInt(channel, 16) / 255)
+ .map((channel) => (channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4));
+ return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
+ };
+ const lighter = Math.max(luminance(firstHex), luminance(secondHex));
+ const darker = Math.min(luminance(firstHex), luminance(secondHex));
+ return (lighter + 0.05) / (darker + 0.05);
+}
+
+describe("Therapy Compass responsive contract", () => {
+ it("defines one scoped phone reflow and a local comparison scroller", () => {
+ expect(stylesSource).toMatch(/@media \(max-width: 640px\)/);
+ expect(stylesSource).toContain(".tc-root .tc-mobile-stack { grid-template-columns: minmax(0, 1fr) !important; }");
+ expect(stylesSource).toContain(".tc-root .tc-mobile-grid-2");
+ expect(stylesSource).toContain(".tc-root .tc-mobile-static");
+ expect(stylesSource).toContain(".tc-root .tc-home-search");
+ expect(stylesSource).toContain(".tc-root .tc-compare-table");
+ expect(stylesSource).toContain("overflow-x: auto !important;");
+ });
+
+ it("marks every fixed screen/card grid for phone reflow without changing its desktop template", () => {
+ expect(classCount(therapyCardSource, "tc-mobile-stack")).toBeGreaterThanOrEqual(2);
+ expect(classCount(homeSource, "tc-mobile-stack")).toBeGreaterThanOrEqual(3);
+ expect(homeSource).toContain('className="tc-home-search"');
+ expect(homeSource).toContain("tc-home-search-button");
+ expect(classCount(detailSource, "tc-mobile-stack")).toBeGreaterThanOrEqual(2);
+ expect(detailSource).toContain('className="tc-mobile-static"');
+ expect(compareSource).toContain('className="tc-mobile-stack"');
+ expect(compareSource).toContain('className="tc-compare-tabs"');
+ expect(compareSource).toContain('className="tc-compare-table tc-scroll"');
+ expect(classCount(recommendSource, "tc-mobile-stack")).toBeGreaterThanOrEqual(2);
+ expect(pathwaysSource).toContain('className="tc-mobile-stack"');
+ expect(pathwaysSource).toContain('className="tc-pathway-list"');
+ expect(classCount(briefSource, "tc-mobile-stack")).toBeGreaterThanOrEqual(2);
+ expect(briefSource).toContain('className="tc-mobile-grid-2"');
+ expect(sheetsSource).toContain('className="tc-mobile-stack"');
+ expect(sheetsSource).toContain("tc-builder-panel tc-mobile-static");
+ expect(otherSource).toContain('className="tc-mobile-stack"');
+
+ expect(therapyCardSource).toContain("grid-template-columns:minmax(280px,1fr) minmax(400px,1.35fr) auto");
+ expect(detailSource).toContain("grid-template-columns:minmax(0,1fr) 344px");
+ expect(pathwaysSource).toContain("grid-template-columns:320px minmax(0,1fr)");
+ expect(briefSource).toContain("grid-template-columns:300px minmax(0,1fr)");
+ expect(sheetsSource).toContain("grid-template-columns:340px minmax(0,1fr)");
+ });
+
+ it("renders the unavailable Favourite action honestly disabled", () => {
+ const favouriteButton = therapyCardSource.match(
+ //,
+ )?.[0];
+
+ expect(favouriteButton).toBeTruthy();
+ expect(favouriteButton).toContain("disabled");
+ expect(favouriteButton).toContain('aria-label="Favourite saving is not available yet"');
+ expect(favouriteButton).toContain("cursor:not-allowed");
+ expect(favouriteButton).not.toContain("onClick");
+ });
+});
+
+describe("clinical accent contrast contract", () => {
+ it("uses the semantic contrast token on every identified accent foreground", () => {
+ const sources = [
+ read(`${therapyPath}/controls.ts`),
+ read(`${therapyPath}/ui.tsx`),
+ homeSource,
+ recommendSource,
+ pathwaysSource,
+ briefSource,
+ read("src/components/clinical-dashboard/answer-status.tsx"),
+ ];
+
+ for (const source of sources) {
+ expect(source).not.toMatch(/background:var\(--clinical-accent\);color:#(?:fff|ffffff)/i);
+ expect(source).not.toMatch(/bg-\[color:var\(--clinical-accent\)\][^"\n]*\btext-white\b/);
+ expect(source).toContain("clinical-accent-contrast");
+ }
+ expect(pathwaysSource).not.toContain('? "#fff" : "var(--clinical-accent)"');
+ expect(briefSource).not.toContain('? "#fff" : "var(--clinical-accent)"');
+ });
+
+ it("keeps the current dark accent/foreground token pair above text contrast", () => {
+ const globalsSource = read("src/app/globals.css");
+ const darkStart = globalsSource.indexOf(".dark {");
+ const darkEnd = globalsSource.indexOf("\n}", darkStart);
+ const darkTokens = globalsSource.slice(darkStart, darkEnd);
+ const accent = darkTokens.match(/--primary-500:\s*(#[0-9a-f]{6})/i)?.[1];
+ const foreground = darkTokens.match(/--clinical-accent-contrast:\s*(#[0-9a-f]{6})/i)?.[1];
+
+ expect(accent).toBeTruthy();
+ expect(foreground).toBeTruthy();
+ expect(contrastRatio(accent!, foreground!)).toBeGreaterThanOrEqual(4.5);
+ });
+});
+
+describe("VisualEvidence unavailable-source semantics", () => {
+ it("renders a non-link row when a source href is absent", () => {
+ const source = read("src/components/clinical-dashboard/visual-evidence.tsx");
+
+ expect(source).not.toContain('href={row.href ?? "#"}');
+ expect(source).not.toContain("aria-disabled={!row.href}");
+ expect(source).toContain("if (!row.href)");
+ expect(source).toContain('data-testid="evidence-map-source-unavailable"');
+ expect(source).toContain("Source unavailable");
+ expect(source).toContain("href={row.href}");
+ expect(source).toContain('data-testid="evidence-map-open-source"');
+ });
+});
diff --git a/tests/ui-accessibility.spec.ts b/tests/ui-accessibility.spec.ts
index e18ffd1c1..5deada490 100644
--- a/tests/ui-accessibility.spec.ts
+++ b/tests/ui-accessibility.spec.ts
@@ -11,7 +11,41 @@ const readySetupChecks = [
];
const uiAssertionTimeoutMs = 5_000;
+async function blockExternalRequests(page: Page) {
+ await page.route("**/*", async (route) => {
+ const url = new URL(route.request().url());
+ if (
+ (url.protocol === "http:" || url.protocol === "https:") &&
+ !["localhost", "127.0.0.1", "::1", "[::1]"].includes(url.hostname)
+ ) {
+ await route.abort("blockedbyclient");
+ return;
+ }
+ await route.fallback();
+ });
+}
+
async function mockMinimalDashboardApi(page: Page) {
+ await blockExternalRequests(page);
+ await page.route(/\/api\/local-project-id$/, async (route) => {
+ await route.fulfill({
+ json: {
+ appName: "Clinical KB",
+ projectId: "test-project",
+ identityPath: "/api/local-project-id",
+ localServer: {
+ currentUrl: "http://localhost:4298",
+ currentPort: 4298,
+ projectPortStart: 4298,
+ projectPortEnd: 53210,
+ safeLocalOrigin: true,
+ requestOrigin: null,
+ requestReferer: null,
+ unsafeLocalCaller: null,
+ },
+ },
+ });
+ });
await page.route("**/api/setup-status**", async (route) => {
await route.fulfill({
json: { demoMode: false, checks: readySetupChecks },
@@ -31,10 +65,60 @@ async function mockMinimalDashboardApi(page: Page) {
await page.route(/\/api\/ingestion\/batches(?:\?.*)?$/, async (route) => {
await route.fulfill({ json: { batches: [] } });
});
+ await page.route(/\/api\/ingestion\/quality(?:\?.*)?$/, async (route) => {
+ await route.fulfill({ json: { items: [] } });
+ });
+ await page.route(/\/api\/registry\/records(?:\?.*)?$/, async (route) => {
+ await route.fulfill({ json: { records: [], total: 0, governance: {} } });
+ });
+ await page.route(/\/api\/search\/universal(?:\?.*)?$/, async (route) => {
+ await route.fulfill({
+ json: {
+ query: "",
+ contextMode: "differentials",
+ preferredDomains: [],
+ domainOrder: [],
+ groups: [],
+ },
+ });
+ });
+}
+
+async function mockDifferentialSearch(page: Page) {
+ await page.route(/\/api\/search(?:\?.*)?$/, async (route) => {
+ await route.fulfill({
+ json: {
+ results: [],
+ visualEvidence: [],
+ relatedDocuments: [],
+ documentMatches: [
+ {
+ document_id: "11111111-1111-4111-8111-111111111111",
+ title: "Acute confusion differential guide",
+ file_name: "acute-confusion-differentials.pdf",
+ labels: [],
+ summarySnippet: "Reviewed acute confusion differential guidance.",
+ bestPages: [1],
+ bestChunkIds: ["chunk-acute-confusion"],
+ imageCount: 0,
+ tableCount: 0,
+ matchReason: "Matched indexed passage",
+ score: 0.93,
+ },
+ ],
+ relevance: { verdict: "strong", score: 0.91, directSourceCount: 1, weakSourceCount: 0 },
+ smartPanel: {},
+ telemetry: { query_class: "differential_compare", retrieval_strategy: "text_fast_path" },
+ scope: { queryMode: "compare_guidance" },
+ sourceGovernanceWarnings: [],
+ demoMode: true,
+ },
+ });
+ });
}
-async function gotoApp(page: Page) {
- await page.goto("/", { waitUntil: "domcontentloaded" });
+async function gotoApp(page: Page, path = "/") {
+ await page.goto(path, { waitUntil: "domcontentloaded" });
await expect(page.locator("#main-content").first()).toBeVisible({ timeout: 15_000 });
}
@@ -94,7 +178,7 @@ async function expectNoBlockingAxeViolations(page: Page, testInfo: TestInfo, opt
expect(summary, "axe found critical/serious WCAG A/AA violations").toEqual([]);
}
-test.describe("Clinical KB accessibility media smoke", () => {
+test.describe("Clinical KB accessibility coverage", () => {
test.describe.configure({ timeout: 60_000 });
test("dashboard remains usable with reduced motion", async ({ page }) => {
@@ -203,4 +287,91 @@ test.describe("Clinical KB accessibility media smoke", () => {
// author colors); contrast is asserted by the default-colors scan instead.
await expectNoBlockingAxeViolations(page, testInfo, { disableRules: ["color-contrast"] });
});
+
+ test("differential result types are pressed filters instead of tabs", async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await mockMinimalDashboardApi(page);
+ await mockDifferentialSearch(page);
+ await gotoApp(page, "/differentials");
+
+ await page.locator('input[placeholder="Ask or search a presentation"]:visible').first().fill("acute confusion");
+ await page.locator('button[aria-label="Search differential presentations"]:visible').click();
+ await expect(page.getByTestId("differentials-search-results")).toBeVisible();
+
+ const filterGroup = page.getByRole("group", { name: "Result type" });
+ await expect(filterGroup).toBeVisible();
+ await expect(filterGroup.getByRole("tab")).toHaveCount(0);
+ const allFilter = filterGroup.getByRole("button", { name: /^All \(\d+\)$/ });
+ const presentationsFilter = filterGroup.getByRole("button", { name: /^Presentations \(\d+\)$/ });
+ const diagnosesFilter = filterGroup.getByRole("button", { name: /^Diagnoses \(\d+\)$/ });
+ await expect(allFilter).toHaveAttribute("aria-pressed", "true");
+ await expect(presentationsFilter).toHaveAttribute("aria-pressed", "false");
+
+ await presentationsFilter.focus();
+ await page.keyboard.press("Space");
+ await expect(presentationsFilter).toHaveAttribute("aria-pressed", "true");
+ await expect(allFilter).toHaveAttribute("aria-pressed", "false");
+
+ await diagnosesFilter.focus();
+ await page.keyboard.press("Enter");
+ await expect(diagnosesFilter).toHaveAttribute("aria-pressed", "true");
+ await expect(presentationsFilter).toHaveAttribute("aria-pressed", "false");
+ });
+
+ test("upload and indexing tabs expose panel associations and roving keyboard activation", async ({ page }) => {
+ await page.setViewportSize({ width: 414, height: 820 });
+ await mockMinimalDashboardApi(page);
+ await gotoApp(page);
+
+ const uploadButton = page.getByRole("button", { name: /Upload document/i });
+ await expect(uploadButton).toBeVisible();
+ await uploadButton.click();
+ const drawer = page.getByRole("dialog", { name: "Upload and indexing" });
+ await expect(drawer).toBeVisible();
+
+ const tablist = drawer.getByRole("tablist", { name: "Upload and indexing sections" });
+ const setupTab = tablist.getByRole("tab", { name: "Setup", exact: true });
+ const uploadTab = tablist.getByRole("tab", { name: "Upload", exact: true });
+ const jobsTab = tablist.getByRole("tab", { name: "Jobs", exact: true });
+ const qualityTab = tablist.getByRole("tab", { name: "Quality", exact: true });
+ const associations = [
+ [setupTab, "dashboard-setup-section"],
+ [uploadTab, "dashboard-upload-section"],
+ [jobsTab, "dashboard-indexing-section"],
+ [qualityTab, "dashboard-quality-section"],
+ ] as const;
+
+ for (const [tab, panelId] of associations) {
+ const tabId = await tab.getAttribute("id");
+ expect(tabId).toBeTruthy();
+ await expect(tab).toHaveAttribute("aria-controls", panelId);
+ await expect(drawer.locator(`#${panelId}`)).toHaveAttribute("aria-labelledby", tabId!);
+ }
+
+ await expect(uploadTab).toHaveAttribute("aria-selected", "true");
+ await expect(uploadTab).toHaveAttribute("tabindex", "0");
+ await expect(jobsTab).toHaveAttribute("tabindex", "-1");
+ await uploadTab.focus();
+
+ await page.keyboard.press("ArrowRight");
+ await expect(jobsTab).toBeFocused();
+ await expect(jobsTab).toHaveAttribute("aria-selected", "true");
+ await expect(drawer.locator("#dashboard-indexing-section")).toBeVisible();
+
+ await page.keyboard.press("ArrowLeft");
+ await expect(uploadTab).toBeFocused();
+ await expect(uploadTab).toHaveAttribute("aria-selected", "true");
+
+ await page.keyboard.press("Home");
+ await expect(setupTab).toBeFocused();
+ await expect(setupTab).toHaveAttribute("aria-selected", "true");
+
+ await page.keyboard.press("End");
+ await expect(qualityTab).toBeFocused();
+ await expect(qualityTab).toHaveAttribute("aria-selected", "true");
+
+ await page.keyboard.press("ArrowRight");
+ await expect(setupTab).toBeFocused();
+ await expect(setupTab).toHaveAttribute("aria-selected", "true");
+ });
});
diff --git a/tests/ui-overlay-css-contract.test.ts b/tests/ui-overlay-css-contract.test.ts
new file mode 100644
index 000000000..a87f02ae0
--- /dev/null
+++ b/tests/ui-overlay-css-contract.test.ts
@@ -0,0 +1,32 @@
+import { readFileSync } from "node:fs";
+
+import { describe, expect, it } from "vitest";
+
+const read = (relativePath: string) => readFileSync(new URL(`../${relativePath}`, import.meta.url), "utf8");
+const answerResultSurfaceSource = read("src/components/clinical-dashboard/answer-result-surface.tsx");
+const sheetSource = read("src/components/ui/sheet.tsx");
+const globalStylesSource = read("src/app/globals.css");
+
+function occurrenceCount(source: string, value: string) {
+ return source.split(value).length - 1;
+}
+
+describe("overlay and global CSS contracts", () => {
+ it("uses the Sheet semantic backdrop without answer-surface call-site overrides", () => {
+ expect(sheetSource).toContain("bg-[color:var(--overlay-backdrop)]");
+ expect(answerResultSurfaceSource).not.toContain("desktopBackdropClassName=");
+ expect(answerResultSurfaceSource).not.toContain("sm:bg-black/50");
+ });
+
+ it("defines the shared easing tokens only once", () => {
+ expect(occurrenceCount(globalStylesSource, "--ease-standard:")).toBe(1);
+ expect(occurrenceCount(globalStylesSource, "--ease-emphasized:")).toBe(1);
+ });
+
+ it("keeps one authoritative 430px answer-action sizing block", () => {
+ expect(occurrenceCount(globalStylesSource, "@media (max-width: 430px)")).toBe(1);
+ expect(globalStylesSource).toMatch(
+ /@media \(max-width: 430px\) \{[\s\S]*?\.answer-footer-search-action,[\s\S]*?min-height: 2\.75rem;[\s\S]*?\.answer-footer-search-action svg,[\s\S]*?height: 1\.1rem;/,
+ );
+ });
+});
diff --git a/tests/ui-route-coverage.spec.ts b/tests/ui-route-coverage.spec.ts
new file mode 100644
index 000000000..fe5826cc0
--- /dev/null
+++ b/tests/ui-route-coverage.spec.ts
@@ -0,0 +1,413 @@
+import { resolve } from "node:path";
+import type { Route } from "playwright-core";
+import { expect, test, type Page } from "playwright/test";
+
+import { demoDocuments, getDemoDocument, getDemoDocumentPayload } from "../src/lib/demo-data";
+import {
+ differentialDiagnosesCards,
+ getDifferentialDetailContext,
+ getDifferentialRecord,
+} from "../src/lib/differentials";
+import { loadMedicationSnapshot } from "../src/lib/medication-snapshot";
+
+const routeViewports = [
+ { name: "desktop", width: 1280, height: 900 },
+ { name: "phone", width: 390, height: 844 },
+] as const;
+
+const readySetupChecks = [
+ { id: "env", label: ".env.local configured", status: "ready", detail: "Local route fixture ready." },
+ { id: "project", label: "Clinical KB Database target", status: "ready", detail: "Local route fixture ready." },
+ { id: "schema", label: "supabase/schema.sql applied", status: "ready", detail: "Local route fixture ready." },
+ { id: "search", label: "Search RPC and vector indexes", status: "ready", detail: "Local route fixture ready." },
+ { id: "openai", label: "OpenAI API key available", status: "ready", detail: "Not used by this test." },
+ { id: "worker", label: "npm run worker running", status: "unknown", detail: "Not required by this test." },
+];
+
+const problemsByPage = new WeakMap();
+
+async function blockExternalRequests(page: Page, problems: string[]) {
+ await page.route("**/*", async (route) => {
+ const url = new URL(route.request().url());
+ if (
+ (url.protocol === "http:" || url.protocol === "https:") &&
+ !["localhost", "127.0.0.1", "::1", "[::1]"].includes(url.hostname)
+ ) {
+ problems.push(`external ${route.request().method()} ${url.origin}${url.pathname}`);
+ await route.abort("blockedbyclient");
+ return;
+ }
+ await route.fallback();
+ });
+}
+
+async function fulfillDocumentRequest(route: Route, pathname: string, url: URL) {
+ const signedUrlMatch = pathname.match(/^\/api\/documents\/([^/]+)\/signed-url$/);
+ if (signedUrlMatch) {
+ const document = getDemoDocument(decodeURIComponent(signedUrlMatch[1]));
+ if (!document) {
+ await route.fulfill({ status: 404, json: { error: "Local demo document not found." } });
+ return true;
+ }
+ await route.fulfill({
+ json: { url: document.storage_path, fileType: document.file_type, demoMode: true },
+ });
+ return true;
+ }
+
+ const documentMatch = pathname.match(/^\/api\/documents\/([^/]+)$/);
+ if (!documentMatch) return false;
+ const payload = getDemoDocumentPayload(
+ decodeURIComponent(documentMatch[1]),
+ url.searchParams.get("chunk") ?? undefined,
+ );
+ if (!payload) {
+ await route.fulfill({ status: 404, json: { error: "Local demo document not found." } });
+ return true;
+ }
+ await route.fulfill({ json: { ...payload, demoMode: true } });
+ return true;
+}
+
+async function installOfflineApiFixtures(page: Page, problems: string[]) {
+ await page.route("**/api/**", async (route) => {
+ const url = new URL(route.request().url());
+ const { pathname } = url;
+
+ if (pathname === "/api/local-project-id") {
+ await route.fulfill({
+ json: {
+ appName: "Clinical KB",
+ projectId: "route-coverage-fixture",
+ identityPath: "/api/local-project-id",
+ localServer: { safeLocalOrigin: true },
+ },
+ });
+ return;
+ }
+ if (pathname === "/api/setup-status") {
+ await route.fulfill({ json: { demoMode: true, checks: readySetupChecks } });
+ return;
+ }
+ if (pathname === "/api/medications") {
+ const records = loadMedicationSnapshot();
+ await route.fulfill({ json: { records, total: records.length, governance: {}, demoMode: true } });
+ return;
+ }
+ if (pathname === "/api/documents") {
+ await route.fulfill({
+ json: {
+ documents: demoDocuments,
+ demoMode: true,
+ pagination: {
+ limit: 150,
+ offset: 0,
+ total: demoDocuments.length,
+ nextOffset: demoDocuments.length,
+ hasMore: false,
+ },
+ },
+ });
+ return;
+ }
+ if (/^\/api\/ingestion\/(jobs|batches|quality)$/.test(pathname)) {
+ await route.fulfill({ json: { jobs: [], batches: [], items: [], demoMode: true } });
+ return;
+ }
+ if (pathname === "/api/registry/records") {
+ await route.fulfill({ json: { records: [], total: 0, governance: {}, demoMode: true } });
+ return;
+ }
+ const differentialMatch = pathname.match(/^\/api\/differentials\/([^/]+)$/);
+ if (differentialMatch) {
+ const record = getDifferentialRecord(decodeURIComponent(differentialMatch[1]));
+ if (!record) {
+ await route.fulfill({ status: 404, json: { error: "Local differential fixture not found." } });
+ return;
+ }
+ await route.fulfill({
+ json: {
+ record,
+ detailContext: getDifferentialDetailContext(record),
+ governance: { sourceStatus: "current", validationStatus: "approved" },
+ demoMode: true,
+ },
+ });
+ return;
+ }
+ if (await fulfillDocumentRequest(route, pathname, url)) return;
+
+ problems.push(`api ${route.request().method()} ${pathname}`);
+ await route.abort("blockedbyclient");
+ });
+}
+
+async function installTherapyFixtures(page: Page) {
+ await page.route("**/therapy-compass-data/*.json", async (route) => {
+ const filename = new URL(route.request().url()).pathname.split("/").at(-1) ?? "";
+ if (!new Set(["therapies.json", "pathways.json", "reference.json"]).has(filename)) {
+ await route.abort("blockedbyclient");
+ return;
+ }
+ await route.fulfill({
+ contentType: "application/json",
+ path: resolve(process.cwd(), "public", "therapy-compass-data", filename),
+ });
+ });
+}
+
+async function gotoApp(page: Page, path: string) {
+ await page.goto(path, { waitUntil: "domcontentloaded" });
+ await page
+ .locator("#main-content")
+ .first()
+ .waitFor({ state: "visible", timeout: 20_000 })
+ .catch(() => undefined);
+}
+
+async function expectNoHorizontalOverflow(page: Page) {
+ await expect
+ .poll(
+ () =>
+ page.evaluate(() => {
+ const documentWidth = Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0);
+ return documentWidth - document.documentElement.clientWidth;
+ }),
+ { timeout: 5_000 },
+ )
+ .toBeLessThanOrEqual(2);
+}
+
+async function proveRenderedRoute(
+ page: Page,
+ path: string,
+ assertReady: (page: Page) => Promise,
+ provePhoneAction: (page: Page) => Promise,
+) {
+ for (const viewport of routeViewports) {
+ await page.setViewportSize(viewport);
+ await gotoApp(page, path);
+ await assertReady(page);
+ await expectNoHorizontalOverflow(page);
+ }
+ await provePhoneAction(page);
+}
+
+test.describe("previously uncovered production routes", () => {
+ test.describe.configure({ timeout: 120_000 });
+
+ test.beforeEach(async ({ page }) => {
+ const problems: string[] = [];
+ problemsByPage.set(page, problems);
+ page.on("pageerror", (error) => problems.push(`pageerror ${error.message}`));
+ await blockExternalRequests(page, problems);
+ await installOfflineApiFixtures(page, problems);
+ await installTherapyFixtures(page);
+ });
+
+ test.afterEach(async ({ page }) => {
+ expect(
+ problemsByPage.get(page) ?? [],
+ "route made an unmocked API/external request or raised a page error",
+ ).toEqual([]);
+ });
+
+ test("Therapy Compass renders responsively and opens its local search", async ({ page }) => {
+ await proveRenderedRoute(
+ page,
+ "/therapy-compass",
+ async (currentPage) => {
+ await expect(currentPage.getByRole("main")).toBeVisible();
+ await expect(
+ currentPage.getByRole("heading", { name: "What therapy are you looking for?", level: 1 }),
+ ).toBeVisible({ timeout: 30_000 });
+ },
+ async (currentPage) => {
+ const search = currentPage
+ .getByRole("navigation", { name: "Therapy Compass sections" })
+ .getByRole("button", { name: "Search", exact: true });
+ await expect(search).toBeEnabled();
+ await search.click();
+ await expect(currentPage.getByRole("heading", { name: "Therapy Search", level: 1 })).toBeVisible();
+ },
+ );
+ });
+
+ test("DSM home renders responsively and opens comparison", async ({ page }) => {
+ await proveRenderedRoute(
+ page,
+ "/dsm",
+ async (currentPage) => {
+ await expect(currentPage.getByTestId("dsm-home-main")).toBeVisible();
+ await expect(currentPage.getByRole("heading", { name: "DSM-5 Diagnosis", level: 1 })).toBeVisible();
+ },
+ async (currentPage) => {
+ const compare = currentPage.getByTestId("dsm-home-compare");
+ await expect(compare).toBeEnabled();
+ await compare.click();
+ await expect(currentPage).toHaveURL(/\/dsm\/compare$/);
+ await expect(currentPage.getByRole("heading", { name: "Compare DSM diagnoses", level: 1 })).toBeVisible();
+ },
+ );
+ });
+
+ test("DSM comparison renders responsively and removes a selected diagnosis", async ({ page }) => {
+ await proveRenderedRoute(
+ page,
+ "/dsm/compare?ids=major-depressive-disorder,bipolar-ii-disorder",
+ async (currentPage) => {
+ await expect(currentPage.getByTestId("dsm-comparison-page")).toBeVisible();
+ await expect(currentPage.getByRole("heading", { name: "Compare DSM diagnoses", level: 1 })).toBeVisible();
+ },
+ async (currentPage) => {
+ const remove = currentPage.getByRole("link", {
+ name: "Remove Major depressive disorder from comparison",
+ });
+ await expect(remove).toBeEnabled();
+ await remove.click();
+ await expect(currentPage).toHaveURL(/\/dsm\/compare\?ids=bipolar-ii-disorder$/);
+ await expect(currentPage.getByRole("heading", { name: "Choose at least two diagnoses" })).toBeVisible();
+ },
+ );
+ });
+
+ test("DSM differential considerations render responsively and change review lens", async ({ page }) => {
+ await proveRenderedRoute(
+ page,
+ "/dsm/diagnoses/major-depressive-disorder/differentials",
+ async (currentPage) => {
+ await expect(currentPage.getByTestId("dsm-differential-considerations-page")).toBeVisible();
+ await expect(currentPage.getByRole("heading", { name: "Major depressive disorder", level: 1 })).toBeVisible();
+ },
+ async (currentPage) => {
+ const medicalLens = currentPage.getByRole("button", { name: /Substance \/ medical/ });
+ await expect(medicalLens).toBeEnabled();
+ await medicalLens.click();
+ await expect(medicalLens).toHaveAttribute("aria-pressed", "true");
+ },
+ );
+ });
+
+ test("Specifier comparison renders responsively and swaps both selections", async ({ page }) => {
+ await proveRenderedRoute(
+ page,
+ "/specifiers/compare?a=with-mixed-features&b=with-anxious-distress",
+ async (currentPage) => {
+ await expect(currentPage.getByRole("main")).toBeVisible();
+ await expect(currentPage.getByRole("heading", { name: "Compare specifiers", level: 1 })).toBeVisible();
+ },
+ async (currentPage) => {
+ const selects = currentPage.locator("select");
+ const before = await selects.evaluateAll((items) => items.map((item) => (item as HTMLSelectElement).value));
+ const swap = currentPage.getByRole("button", { name: "Swap compared specifiers" });
+ await expect(swap).toBeEnabled();
+ await swap.click();
+ await expect
+ .poll(() => selects.evaluateAll((items) => items.map((item) => (item as HTMLSelectElement).value)))
+ .toEqual([before[1], before[0]]);
+ },
+ );
+ });
+
+ test("Specifier map renders responsively and changes its selected specifier", async ({ page }) => {
+ await proveRenderedRoute(
+ page,
+ "/specifiers/map?selected=with-anxious-distress",
+ async (currentPage) => {
+ await expect(currentPage.getByRole("main")).toBeVisible();
+ await expect(currentPage.getByRole("heading", { name: "Specifier map", level: 1 })).toBeVisible();
+ },
+ async (currentPage) => {
+ const mixedFeatures = currentPage.getByRole("button", { name: "Mixed features" });
+ await expect(mixedFeatures).toBeEnabled();
+ await mixedFeatures.click();
+ await expect(mixedFeatures).toHaveAttribute("aria-pressed", "true");
+ await expect(currentPage.getByRole("heading", { name: "Mixed features", level: 2 })).toBeVisible();
+ },
+ );
+ });
+
+ test("Differential diagnosis stream renders responsively and opens a local entry", async ({ page }) => {
+ const consoleErrors: string[] = [];
+ page.on("console", (message) => {
+ if (message.type() === "error") consoleErrors.push(message.text());
+ });
+ const action =
+ differentialDiagnosesCards.find((card) => !card.href.endsWith("/delirium")) ?? differentialDiagnosesCards[0];
+ await proveRenderedRoute(
+ page,
+ "/differentials/diagnoses?q=delirium",
+ async (currentPage) => {
+ await expect(currentPage.getByRole("main")).toBeVisible();
+ await expect(
+ currentPage.getByRole("heading", {
+ name: "Compare likely causes side-by-side and check exclusion clues.",
+ level: 1,
+ }),
+ ).toBeVisible();
+ },
+ async (currentPage) => {
+ const entry = currentPage.locator(`a[href="${action.href}"]`).first();
+ await expect(entry).toBeVisible();
+ await Promise.all([currentPage.waitForURL(new RegExp(`${action.href}$`), { timeout: 30_000 }), entry.click()]);
+ },
+ );
+ expect(consoleErrors, "differential stream and detail should stay console-error free").toEqual([]);
+ });
+
+ test("colour-coding reference renders responsively and targets main content from the skip link", async ({ page }) => {
+ await proveRenderedRoute(
+ page,
+ "/reference/colour-coding",
+ async (currentPage) => {
+ await expect(currentPage.getByRole("main")).toBeVisible();
+ await expect(currentPage.getByRole("heading", { name: "Colour coding reference", level: 1 })).toBeVisible();
+ },
+ async (currentPage) => {
+ const skipLink = currentPage.getByRole("link", { name: "Skip to main content" });
+ await expect(skipLink).toHaveAttribute("href", "#main-content");
+ await expect(skipLink).toBeEnabled();
+ await skipLink.focus();
+ await expect(skipLink).toBeVisible();
+ await skipLink.press("Enter");
+ await expect(currentPage.locator("#main-content")).toBeFocused();
+ },
+ );
+ });
+
+ test("legacy Applications redirect preserves query parameters at canonical Tools", async ({ page }) => {
+ await gotoApp(page, "/applications?source=legacy&tag=one&tag=two");
+ const destination = new URL(page.url());
+ expect(destination.pathname).toBe("/tools");
+ expect(destination.searchParams.get("source")).toBe("legacy");
+ expect(destination.searchParams.getAll("tag")).toEqual(["one", "two"]);
+ await expect(page.getByRole("heading", { name: "Tools", level: 1 })).toBeVisible();
+ });
+
+ test("Medications index redirects exactly to prescribing mode", async ({ page }) => {
+ await gotoApp(page, "/medications");
+ await expect.poll(() => new URL(page.url()).pathname, { timeout: 30_000 }).toBe("/");
+ const destination = new URL(page.url());
+ expect(destination.pathname).toBe("/");
+ expect(destination.searchParams.toString()).toBe("mode=prescribing");
+ await expect(page.getByRole("button", { name: "Mode Medication" })).toBeVisible({ timeout: 30_000 });
+ });
+
+ test("Document source redirect forwards a valid page and chunk", async ({ page }) => {
+ const id = demoDocuments[0].id;
+ await gotoApp(page, `/documents/source?id=${id}&page=2&chunk=safety%20plan`);
+ await expect.poll(() => new URL(page.url()).pathname, { timeout: 60_000 }).toBe(`/documents/${id}`);
+ const destination = new URL(page.url());
+ expect(destination.pathname).toBe(`/documents/${id}`);
+ expect(destination.searchParams.get("page")).toBe("2");
+ expect(destination.searchParams.get("chunk")).toBe("safety plan");
+ });
+
+ test("Document source evidence alias preserves invalid-id fallback", async ({ page }) => {
+ await gotoApp(page, "/documents/source/evidence?id=not-a-uuid&page=2");
+ await expect.poll(() => new URL(page.url()).pathname, { timeout: 30_000 }).toBe("/documents/search");
+ const destination = new URL(page.url());
+ expect(destination.pathname).toBe("/documents/search");
+ expect(destination.search).toBe("");
+ });
+});
diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts
index f84687920..88f8132b6 100644
--- a/tests/ui-smoke.spec.ts
+++ b/tests/ui-smoke.spec.ts
@@ -1497,9 +1497,29 @@ test.describe("Clinical KB UI smoke coverage", () => {
const clinicalNotesSheet = page.getByRole("dialog", { name: "Clinical notes" });
await expect(clinicalNotesSheet).toBeVisible();
await expect(clinicalNotesSheet.getByTestId("clinical-notes-checklist")).toBeVisible();
- await expect(clinicalNotesSheet.getByRole("tab", { name: /Essentials/ })).toBeVisible();
- await expect(clinicalNotesSheet.getByRole("tab", { name: /Actions/ })).toBeVisible();
- await expect(clinicalNotesSheet.getByRole("tab", { name: /Safety/ })).toBeVisible();
+ const essentialsTab = clinicalNotesSheet.getByRole("tab", { name: /Essentials/ });
+ const actionsTab = clinicalNotesSheet.getByRole("tab", { name: /Actions/ });
+ const safetyTab = clinicalNotesSheet.getByRole("tab", { name: /Safety/ });
+ await expect(essentialsTab).toBeVisible();
+ await expect(actionsTab).toBeVisible();
+ await expect(safetyTab).toBeVisible();
+ await expect(actionsTab).toHaveAttribute("aria-selected", "true");
+ await actionsTab.focus();
+ await page.keyboard.press("ArrowRight");
+ await expect(safetyTab).toBeFocused();
+ await expect(safetyTab).toHaveAttribute("aria-selected", "true");
+ await page.keyboard.press("ArrowLeft");
+ await expect(actionsTab).toBeFocused();
+ await expect(actionsTab).toHaveAttribute("aria-selected", "true");
+ await page.keyboard.press("Home");
+ await expect(essentialsTab).toBeFocused();
+ await expect(essentialsTab).toHaveAttribute("aria-selected", "true");
+ await page.keyboard.press("End");
+ await expect(safetyTab).toBeFocused();
+ await expect(safetyTab).toHaveAttribute("aria-selected", "true");
+ await page.keyboard.press("ArrowLeft");
+ await expect(actionsTab).toBeFocused();
+ await expect(actionsTab).toHaveAttribute("aria-selected", "true");
expect(await clinicalNotesSheet.getByTestId("clinical-note-row").count()).toBeGreaterThan(0);
const linkedNoteRow = clinicalNotesSheet.getByTestId("clinical-note-row").first();
await expect(linkedNoteRow).toHaveAttribute("href", /\/documents\//);
@@ -2631,7 +2651,9 @@ test.describe("Clinical KB UI smoke coverage", () => {
await expect(page.getByTestId("favourites-hub").getByText("13YARN").first()).toBeVisible();
});
- test("favourites command library opens item workspace on row selection at 2xl", async ({ page }) => {
+ test("favourites command library exposes truthful item details and a keyboard-operable action menu", async ({
+ page,
+ }) => {
await page.setViewportSize({ width: 1536, height: 900 });
await mockDemoApi(page);
await gotoApp(page, "/favourites");
@@ -2639,10 +2661,38 @@ test.describe("Clinical KB UI smoke coverage", () => {
await expect(page.getByRole("heading", { name: "Favourites command library" })).toBeVisible();
await expect(page.getByTestId("favourites-item-workspace")).toHaveCount(0);
- await page.getByTestId("favourite-row-acamprosate-renal-screen").click();
+ await page.getByTestId("favourite-row-lithium-monitoring-guideline").locator("button[aria-pressed]").click();
const workspace = page.getByTestId("favourites-item-workspace");
await expect(workspace).toBeVisible();
- await expect(workspace.getByRole("heading", { name: "Acamprosate renal screen", level: 3 })).toBeVisible();
+ await expect(workspace.getByRole("heading", { name: "Lithium monitoring guideline", level: 3 })).toBeVisible();
+
+ await workspace.getByRole("button", { name: "Evidence" }).click();
+ await expect(workspace).not.toContainText("BNF - Acamprosate");
+ await workspace.getByRole("button", { name: "Notes" }).click();
+ await expect(workspace).toContainText("No personal note is saved for this item.");
+
+ const moreActions = page.getByRole("button", { name: "More actions for Lithium monitoring guideline" });
+ await moreActions.focus();
+ await page.keyboard.press("ArrowDown");
+ const menu = page.getByRole("menu");
+ await expect(menu.getByRole("menuitem", { name: "Ask Lithium monitoring guideline" })).toBeFocused();
+ await page.keyboard.press("ArrowDown");
+ await expect(menu.getByRole("menuitem", { name: "Copy citation" })).toBeFocused();
+ await page.keyboard.press("Enter");
+ await expect(menu.getByRole("menuitem", { name: "Copied" })).toBeFocused();
+ await page.keyboard.press("Escape");
+ await expect(moreActions).toBeFocused();
+ });
+
+ test("favourites mobile cards keep their links and menus as separate native controls", async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await mockDemoApi(page);
+ await gotoApp(page, "/favourites");
+
+ const hub = page.getByTestId("favourites-hub");
+ await expect(hub.locator('article[role="button"]')).toHaveCount(0);
+ await expect(hub.getByRole("link", { name: "Open Acamprosate renal screen" })).toBeVisible();
+ await expect(hub.getByRole("button", { name: "More actions for Acamprosate renal screen" })).toBeVisible();
});
test("mobile favourite cards keep selection and actions as independent touch controls", async ({ page }) => {
@@ -2910,13 +2960,37 @@ test.describe("Clinical KB UI smoke coverage", () => {
"Medication Prescribing",
);
await expect(page.getByTestId("tools-hub").getByText("Selected tool")).toHaveCount(0);
- await expect(page.getByTestId("tools-hub").getByTestId("application-row-medication-prescribing")).toHaveAttribute(
- "href",
- "/?mode=prescribing",
- );
+ const detailsButton = page
+ .getByTestId("tools-hub")
+ .getByRole("button", { name: "View details for Medication Prescribing" });
+ await expect(detailsButton).toHaveAttribute("aria-haspopup", "dialog");
+ await detailsButton.click();
+ await expect(
+ page.getByRole("dialog", { name: "Medication Prescribing" }).locator('a[href="/?mode=prescribing"]').first(),
+ ).toBeVisible();
await expectNoPageHorizontalOverflow(page);
});
+ test("services decision rail does not present unfinished actions as available", async ({ page }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await mockDemoApi(page);
+ await gotoApp(page, "/services?q=mental%20health&focus=1&run=1");
+
+ const navigator = page.getByRole("main");
+ await expect(navigator).toBeVisible();
+ await expect(navigator.getByRole("button", { name: "Edit" })).toBeDisabled();
+ await expect(navigator.getByRole("button", { name: "Review details" })).toBeDisabled();
+ await expect(navigator.getByRole("button", { name: "View details" })).toBeDisabled();
+ const compare = navigator.getByRole("button", { name: /Compare selected/ });
+ await expect(compare).toBeDisabled();
+ await expect(compare).toHaveAttribute("title", "Service comparison is coming soon");
+ const clear = navigator.getByRole("button", { name: "Clear" });
+ await expect(clear).toBeEnabled();
+ await clear.click();
+ await expect(navigator.getByText("Selected services (0)")).toBeVisible();
+ await expect(compare).toHaveAttribute("title", "Select at least two services before comparing");
+ });
+
test("search regressions avoid fetch errors and open viewer hits @critical", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
await mockDemoApi(page);
diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts
index 16c941fa2..af3b55940 100644
--- a/tests/ui-tools.spec.ts
+++ b/tests/ui-tools.spec.ts
@@ -7,6 +7,7 @@ import { loadMedicationSnapshot } from "../src/lib/medication-snapshot";
import { medicationToSearchResult, rankMedicationRecords } from "../src/lib/medications";
import { sortResultItems } from "../src/lib/result-sort";
import { serviceRecords } from "../src/lib/services";
+import { openAppModeMenu } from "./playwright-app-mode";
import { scrollPrimarySurface } from "./playwright-scroll";
const readySetupChecks = [
@@ -258,10 +259,6 @@ async function commandSurfaceOpensAbovePill(page: Page) {
expect(geometry?.dropdownBottom ?? Number.POSITIVE_INFINITY).toBeLessThanOrEqual((geometry?.pillTop ?? 0) + 2);
}
-function launcherLaunchLink(page: Page, title: string) {
- return page.getByRole("link", { name: `Launch ${title}` }).first();
-}
-
async function gotoLauncher(page: Page, path = "/?mode=tools") {
await page.goto(path, { waitUntil: "domcontentloaded" });
await expect(page.locator("#main-content").first()).toBeVisible({ timeout: 15_000 });
@@ -353,18 +350,6 @@ async function expectVerticalSeparation(page: Page, upperSelector: string, lower
expect((metrics?.lowerTop ?? 0) - (metrics?.upperBottom ?? 0)).toBeGreaterThanOrEqual(minimumGap);
}
-async function openAppModeMenu(page: Page, currentMode: string) {
- const trigger = page.getByRole("button", { name: `Mode ${currentMode}` });
- const menu = page.getByRole("menu", { name: "Choose app mode" });
-
- await expect(trigger).toBeVisible();
- await waitForReactEventHandler(trigger);
- await trigger.click();
- await expect(menu).toBeVisible({ timeout: 20_000 });
-
- return menu;
-}
-
test.describe("Clinical KB tools launcher", () => {
test.describe.configure({ timeout: 60_000 });
@@ -391,7 +376,7 @@ test.describe("Clinical KB tools launcher", () => {
await page.getByRole("button", { name: "Close Medication Prescribing" }).click();
await expect(selectedSheet).toBeHidden();
} else {
- await expect(launcherLaunchLink(page, "Clinical KB Search")).toHaveAttribute("href", "/?mode=answer");
+ await expect(page.getByRole("button", { name: "View details for Clinical KB Search" })).toBeVisible();
}
await expect(page.getByLabel("Mode Tools")).toBeVisible();
await expect(visibleGlobalSearchInput(page)).toHaveCount(1);
@@ -421,14 +406,21 @@ test.describe("Clinical KB tools launcher", () => {
await page.setViewportSize({ width: 1280, height: 900 });
await gotoLauncher(page);
- const medicationLink = launcherLaunchLink(page, "Medication Prescribing");
- await expect(medicationLink).toHaveAttribute("href", "/?mode=prescribing");
- await expect(medicationLink).not.toHaveAttribute("target", "_blank");
- await expect(launcherLaunchLink(page, "Documents")).toHaveAttribute("href", "/?mode=documents");
- await expect(launcherLaunchLink(page, "Services")).toHaveAttribute("href", "/services");
- await expect(launcherLaunchLink(page, "Forms")).toHaveAttribute("href", "/forms");
- await expect(launcherLaunchLink(page, "Saved workflows")).toHaveAttribute("href", "/favourites");
- await expect(launcherLaunchLink(page, "Clinical KB Search")).toHaveAttribute("href", "/?mode=answer");
+ for (const [title, href] of [
+ ["Medication Prescribing", "/?mode=prescribing"],
+ ["Documents", "/?mode=documents"],
+ ["Services", "/services"],
+ ["Forms", "/forms"],
+ ["Saved workflows", "/favourites"],
+ ["Clinical KB Search", "/?mode=answer"],
+ ] as const) {
+ const detailsButton = page.getByRole("button", { name: `View details for ${title}` });
+ await expect(detailsButton).toHaveAttribute("aria-haspopup", "dialog");
+ await detailsButton.click();
+ const dialog = page.getByRole("dialog", { name: title });
+ await expect(dialog.locator(`a[href="${href}"]`).first()).toBeVisible();
+ await page.getByRole("button", { name: `Close ${title}` }).click();
+ }
// External companion-app launchers were removed; no localhost links should remain.
await expect(page.locator('a[href^="http://localhost"], a[href^="http://127.0.0.1"]')).toHaveCount(0);
});
@@ -1271,6 +1263,8 @@ test.describe("Clinical KB tools launcher", () => {
await expect(page.getByTestId("differentials-search-results")).toBeVisible();
const tabs = page.getByTestId("differential-result-type-tabs");
await expect(tabs).toBeVisible();
+ await expect(tabs).toHaveAttribute("role", "group");
+ await expect(tabs).toHaveAttribute("aria-label", "Result type");
await expect(tabs.getByRole("button", { name: /All \(\d+\)/ })).toBeVisible();
await expect(tabs.getByRole("button", { name: /Presentations \(\d+\)/ })).toBeVisible();
await expect(tabs.getByRole("button", { name: /Diagnoses \(\d+\)/ })).toBeVisible();
@@ -1367,6 +1361,8 @@ test.describe("Clinical KB tools launcher", () => {
await expect(page.getByTestId("differentials-search-results")).toBeVisible();
const tabs = page.getByTestId("differential-result-type-tabs");
await expect(tabs).toBeVisible();
+ await expect(tabs).toHaveAttribute("role", "group");
+ await expect(tabs).toHaveAttribute("aria-label", "Result type");
await expect(tabs.getByRole("button", { name: "All (8)" })).toBeVisible();
await expect(tabs.getByRole("button", { name: "Presentations (1)" })).toBeVisible();
await expect(tabs.getByRole("button", { name: "Diagnoses (7)" })).toBeVisible();
@@ -1549,6 +1545,8 @@ test.describe("Clinical KB tools launcher", () => {
page.getByLabel("Differential review sidebar").getByText("Source pending review").first(),
).toBeVisible();
await expect(page.getByRole("button", { name: "Copy after review" })).toBeVisible();
+ await expect(page.getByRole("button", { name: "Edit columns" })).toBeDisabled();
+ await expect(page.getByText("Long press to reorder. Tap to remove.")).toHaveCount(0);
await expect(page.getByTestId("global-search-input")).toHaveCount(0);
const tableScrolls = await page.getByTestId("differential-comparison-scroll").evaluate((element) => {
@@ -1564,6 +1562,9 @@ test.describe("Clinical KB tools launcher", () => {
await expect(
page.getByRole("heading", { name: `Selected differentials (1 of ${workflow.totalCount})` }).first(),
).toBeVisible();
+ await expect(
+ page.locator("span:visible", { hasText: `+${workflow.totalCount - 1} not selected` }).first(),
+ ).toBeVisible();
await page.setViewportSize({ width: 390, height: 844 });
await gotoLauncher(page, "/differentials/presentations");
@@ -1573,6 +1574,7 @@ test.describe("Clinical KB tools launcher", () => {
await expect(page.getByRole("link", { name: "Compare", exact: true })).toHaveAttribute("aria-current", "page");
await expect(page.getByRole("heading", { level: 1, name: workflow.title })).toBeVisible();
const mobileComparison = page.getByLabel("Mobile differential comparison");
+ await expect(mobileComparison.getByRole("button", { name: "Column filters unavailable" })).toBeDisabled();
await expect(mobileComparison.getByText("Delirium", { exact: true }).first()).toBeVisible();
await expect(mobileComparison.getByText("Substance intoxication", { exact: true }).first()).toBeVisible();
await expect(page.getByRole("button", { name: "Open language and region settings" })).toBeVisible();
@@ -1583,16 +1585,16 @@ test.describe("Clinical KB tools launcher", () => {
await expectNoPageHorizontalOverflow(page);
});
- test("tools mode opens tools directly on mobile", async ({ page }) => {
+ test("tools mode opens tool details before navigation on mobile", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 820 });
await gotoLauncher(page, "/?mode=tools");
const toolsHub = page.getByTestId("tools-hub");
await expect(toolsHub.getByText("Selected tool")).toHaveCount(0);
- await expect(toolsHub.getByTestId("application-row-medication-prescribing")).toHaveAttribute(
- "href",
- "/?mode=prescribing",
- );
+ const detailsButton = toolsHub.getByRole("button", { name: "View details for Medication Prescribing" });
+ await expect(detailsButton).toHaveAttribute("aria-haspopup", "dialog");
+ await detailsButton.click();
+ await expect(page.getByRole("dialog", { name: "Medication Prescribing" })).toBeVisible();
await expectNoPageHorizontalOverflow(page);
});
});
diff --git a/tests/ui-universal-search.spec.ts b/tests/ui-universal-search.spec.ts
index 8e6ccb3df..689c3c09f 100644
--- a/tests/ui-universal-search.spec.ts
+++ b/tests/ui-universal-search.spec.ts
@@ -132,6 +132,37 @@ test.describe("universal search typeahead", () => {
await expect(page.getByRole("option", { name: /View all in Differentials/ })).toBeVisible();
});
+ test("exposes the keyboard-highlighted option from the focused combobox", async ({ page }) => {
+ await mockUniversalSearch(page);
+ const input = await openComposer(page);
+ await input.fill("acamprosate");
+
+ const listbox = page.getByRole("listbox", { name: "Documents search suggestions" });
+ await expect(listbox).toBeVisible();
+ await expect(page.getByText("Medications · 1")).toBeVisible();
+ expect(await input.getAttribute("aria-activedescendant")).toBeNull();
+
+ await input.press("ArrowDown");
+ const firstSelectedOption = listbox.getByRole("option", { selected: true });
+ await expect(firstSelectedOption).toHaveCount(1);
+ const firstActiveId = await firstSelectedOption.getAttribute("id");
+ expect(firstActiveId).toBeTruthy();
+ await expect(input).toBeFocused();
+ await expect(input).toHaveAttribute("aria-activedescendant", firstActiveId!);
+
+ await input.press("ArrowDown");
+ const secondSelectedOption = listbox.getByRole("option", { selected: true });
+ await expect(secondSelectedOption).toHaveCount(1);
+ const secondActiveId = await secondSelectedOption.getAttribute("id");
+ expect(secondActiveId).toBeTruthy();
+ expect(secondActiveId).not.toBe(firstActiveId);
+ await expect(input).toHaveAttribute("aria-activedescendant", secondActiveId!);
+
+ await input.press("Escape");
+ await expect(listbox).toBeHidden();
+ expect(await input.getAttribute("aria-activedescendant")).toBeNull();
+ });
+
test("does not count document-only hits as visible Medication rows", async ({ page }) => {
await page.route(/\/api\/search\/universal(?:\?.*)?$/, async (route) => {
await fulfillUniversalSearch(route, {
diff --git a/tests/visual-evidence-tabs.dom.test.tsx b/tests/visual-evidence-tabs.dom.test.tsx
new file mode 100644
index 000000000..9db75dbec
--- /dev/null
+++ b/tests/visual-evidence-tabs.dom.test.tsx
@@ -0,0 +1,139 @@
+import { render, screen, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it, vi } from "vitest";
+
+import { MobileEvidenceSheetContent } from "@/components/clinical-dashboard/visual-evidence";
+import type { AnswerRenderModel } from "@/lib/answer-render-policy";
+import type { RagAnswer } from "@/lib/types";
+import type { AnswerEvidenceMapRow } from "@/lib/ward-output";
+
+const answer: RagAnswer = {
+ answer: "Use the source-backed monitoring plan.",
+ grounded: true,
+ confidence: "medium",
+ citations: [],
+ sources: [],
+};
+
+const renderModel: AnswerRenderModel = {
+ answerText: answer.answer,
+ trust: "medium",
+ allowedBlocks: ["evidenceMap", "quoteCards", "visualEvidence"],
+ primarySources: [],
+ reviewSources: [],
+ evidenceRows: [],
+ quoteCards: [],
+ visualEvidence: [],
+ relatedDocuments: [],
+ bestSource: null,
+ warnings: ["Confirm the monitoring interval against the cited guideline."],
+ tables: [],
+ copyText: answer.answer,
+};
+
+const missingSourceRow: AnswerEvidenceMapRow = {
+ id: "monitoring-gap",
+ section: "Monitoring interval",
+ detail: "Confirm the interval against the source guideline.",
+ supportLevel: "partial",
+ citationCount: 0,
+ sourceStatus: "Source unavailable",
+ bestSourceLabel: "",
+ bestLinkedPassage: "",
+};
+
+function evidenceSheetProps() {
+ return {
+ answer,
+ sources: [],
+ renderModel,
+ visualEvidence: [],
+ answerEvidenceMapRows: [missingSourceRow],
+ sourceGovernanceWarnings: [],
+ demoMode: false,
+ pendingFeedback: null,
+ copiedQuotes: false,
+ onCopyQuotes: vi.fn(),
+ onSubmitFeedback: vi.fn(),
+ onScopeDocument: vi.fn(),
+ };
+}
+
+describe("MobileEvidenceSheetContent tabs (jsdom)", () => {
+ it("uses unique stable tab and panel ids with reciprocal ARIA associations", () => {
+ const props = evidenceSheetProps();
+ const { rerender } = render( );
+ const tablist = screen.getByRole("tablist", { name: "Evidence sections" });
+ const initialTabIds = within(tablist)
+ .getAllByRole("tab")
+ .map((tab) => tab.id);
+
+ for (const tab of within(tablist).getAllByRole("tab")) {
+ const panelId = tab.getAttribute("aria-controls");
+ const panel = panelId ? document.getElementById(panelId) : null;
+
+ expect(tab.id).not.toBe("");
+ expect(panel).toHaveAttribute("role", "tabpanel");
+ expect(panel).toHaveAttribute("aria-labelledby", tab.id);
+ }
+
+ const unavailableRow = screen.getByTestId("evidence-map-source-unavailable");
+ expect(unavailableRow).toHaveTextContent("Source unavailable");
+ expect(within(unavailableRow).queryByRole("link")).not.toBeInTheDocument();
+
+ rerender( );
+ expect(
+ within(screen.getByRole("tablist", { name: "Evidence sections" }))
+ .getAllByRole("tab")
+ .map((tab) => tab.id),
+ ).toEqual(initialTabIds);
+
+ rerender(
+ <>
+
+
+ >,
+ );
+ const allTabIds = screen.getAllByRole("tab").map((tab) => tab.id);
+ const allPanelIds = screen.getAllByRole("tabpanel", { hidden: true }).map((panel) => panel.id);
+
+ expect(new Set(allTabIds).size).toBe(allTabIds.length);
+ expect(new Set(allPanelIds).size).toBe(allPanelIds.length);
+ });
+
+ it("roves one tab stop and auto-activates Left, Right, Home, and End destinations", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ const claims = screen.getByRole("tab", { name: /^Claims/ });
+ const quotes = screen.getByRole("tab", { name: /^Quotes/ });
+ const images = screen.getByRole("tab", { name: /^Images/ });
+ const gaps = screen.getByRole("tab", { name: /^Gaps/ });
+
+ expect(claims).toHaveAttribute("aria-selected", "true");
+ expect(claims).toHaveAttribute("tabindex", "0");
+ for (const tab of [quotes, images, gaps]) {
+ expect(tab).toHaveAttribute("tabindex", "-1");
+ }
+
+ claims.focus();
+ await user.keyboard("{ArrowRight}");
+ expect(quotes).toHaveFocus();
+ expect(quotes).toHaveAttribute("aria-selected", "true");
+ expect(quotes).toHaveAttribute("tabindex", "0");
+ expect(screen.getByTestId("mobile-evidence-panel-quotes")).not.toHaveAttribute("hidden");
+
+ await user.keyboard("{End}");
+ expect(gaps).toHaveFocus();
+ expect(gaps).toHaveAttribute("aria-selected", "true");
+
+ await user.keyboard("{Home}");
+ expect(claims).toHaveFocus();
+ expect(claims).toHaveAttribute("aria-selected", "true");
+
+ await user.keyboard("{ArrowLeft}");
+ expect(gaps).toHaveFocus();
+ expect(gaps).toHaveAttribute("aria-selected", "true");
+ expect(claims).toHaveAttribute("tabindex", "-1");
+ });
+});
From 82ddda90f9434159629e1fc3c536e3de6ee33974 Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Sat, 18 Jul 2026 22:44:58 +0800
Subject: [PATCH 02/21] fix: resolve design audit integration regressions
---
docs/design-audit-2026-07-17.md | 19 +++++--
src/app/applications/route.ts | 9 ++--
src/app/differentials/presentations/route.ts | 19 ++++---
src/app/medications/route.ts | 9 ++--
.../answer-result-surface.tsx | 3 --
.../differential-stream-page.tsx | 2 +-
.../therapy-compass/therapy-card.tsx | 4 +-
.../audit-navigation-auth-regressions.test.ts | 22 ++++++--
...herapy-compass-responsive-contract.test.ts | 24 +++++----
tests/ui-accessibility.spec.ts | 54 +++----------------
tests/ui-route-coverage.spec.ts | 2 +-
tests/ui-smoke.spec.ts | 45 ++++++----------
tests/ui-tools.spec.ts | 13 +++--
13 files changed, 102 insertions(+), 123 deletions(-)
diff --git a/docs/design-audit-2026-07-17.md b/docs/design-audit-2026-07-17.md
index 520b57590..74cddd0fd 100644
--- a/docs/design-audit-2026-07-17.md
+++ b/docs/design-audit-2026-07-17.md
@@ -25,24 +25,25 @@ This pass deliberately built on the 2026-07-15 live design audit instead of repe
| DQA-04 | P2 | Prototype favourites appeared as genuine saved user content in normal mode, including through universal search. | Gated fixtures behind the effective client demo state on the route, hub, command library, and universal search surface. Normal mode now shows only real saved items. | `favourites/page.tsx`, `ClinicalDashboard.tsx`, `favourites-hub.tsx`, `global-search-shell.tsx`, `master-search-header.tsx`, `universal-search-command-surface.tsx` |
| DQA-05 | P2 | Favourites exposed invisible selection below `2xl`, nested/synthetic mobile card controls, no-op edit/sort actions, incomplete menu keyboard behavior, and clipboard focus loss. | Kept explicit native links and separate buttons on mobile, limited selection to the visible workspace, implemented real citation copy with status/focus restoration, added complete menu keyboard behavior, and disabled or relabelled unfinished actions honestly. | `favourites-command-library-page.tsx`, `favourites-hub.tsx` |
| DQA-06 | P2 | Differential presentation controls advertised editing, filtering, reorder/remove behavior, and a fixed `+2` count that did not exist. | Removed false instructions, derived the remaining count from state, and disabled unfinished controls with availability explanations while preserving the read-only comparison. | `differential-presentation-workflow-page.tsx` |
-| DQA-07 | P2 | Services selection led to enabled-looking Edit/Review/View controls that did nothing, while Compare stayed unavailable without an accurate reason. A later diff briefly disabled the real Clear action. | Disabled unfinished actions with truthful titles, made Compare messaging state-aware, and restored Clear so selected services can be reset. | `services-navigator-page.tsx`, `ui-smoke.spec.ts` |
+| DQA-07 | P2 | Services selection led to enabled-looking Edit/Review/View controls that did nothing, while Compare stayed unavailable without an accurate reason. A later diff briefly disabled the real Clear action. | Removed the false actions and restored Clear. During current-main integration, the later functional checklist, confidence-detail, and selected-service comparison controls were preserved instead of replacing them with disabled placeholders. | `services-navigator-page.tsx`, `ui-smoke.spec.ts` |
| DQA-08 | P2 | Evidence records with no URL were still keyboard-focusable `href="#"` links announced as “Open source”. | Missing-source rows now render as non-links with unavailable semantics; only valid destinations render a Link. | `visual-evidence.tsx`, `visual-evidence-tabs.dom.test.tsx` |
| DQA-09 | P2 | Therapy cards presented an enabled Favourite button with no handler or state change. | The unavailable feature is now disabled and labelled as coming soon, preventing false save confirmation. | `therapy-card.tsx` |
| DQA-10 | P2 | Route/global error fallbacks did not reliably move focus or announce a route failure; the global fallback was hard-coded light-only. | Added programmatic heading focus and alert announcement, and made the self-contained global fallback system-colour, dark-mode, and forced-colour aware. | `route-error-boundary.tsx`, `global-error.tsx`, route error tests |
| DQA-11 | P2 | Clinical Notes, Visual Evidence, and upload/index tab interfaces lacked complete roving keyboard behavior and reciprocal tab/panel relationships. | Added stable unique IDs, `aria-controls`/`aria-labelledby`, roving `tabIndex`, and Left/Right/Home/End auto-activation. | `ClinicalDashboard.tsx`, `answer-result-surface.tsx`, `visual-evidence.tsx`, UI/DOM tests |
| DQA-12 | P2 | Universal search visually highlighted an item without exposing it through `aria-activedescendant`. | The combobox now points to the active option and tests verify the relationship through keyboard navigation. | `master-search-header.tsx`, `universal-search-command-surface.tsx`, `ui-universal-search.spec.ts` |
| DQA-13 | P2 | Differential filters and several selector groups behaved as toggles but lacked grouped/pressed semantics. | Added labelled groups and stateful `aria-pressed` semantics, with browser accessibility assertions. | `differentials-home.tsx`, `evidence-panels.tsx`, `ui-accessibility.spec.ts` |
-| DQA-14 | P2 | Therapy Compass silently swallowed data-load failure and still implied a fabricated `200+` catalogue count. | Added an explicit failure state and Retry action, removed fabricated totals, and retained real successful/empty states. | `use-therapy-data.ts`, `therapy-compass-page.tsx`, `therapy-compass-data-recovery.dom.test.tsx` |
+| DQA-14 | P2 | Therapy Compass silently swallowed data-load failure and still implied a fabricated `200+` catalogue count. | Added an explicit failure state and Retry action, removed fabricated totals, and retained real successful/empty states. The final integration tests the routed workspace that replaced the legacy single-page wrapper. | `use-therapy-data.ts`, `workspace.tsx`, `therapy-compass-data-recovery.dom.test.tsx` |
| DQA-15 | P2 | Direct-entry Forms “Back to forms” used browser history and could leave the application. | The control now deterministically navigates to `/forms?focus=1`; a DOM regression test verifies the destination. | `form-detail-page.tsx`, `forms-back-navigation.dom.test.tsx` |
| DQA-16 | P2 | Firefox/WebKit production projects unintentionally collected advisory `@mockup` tests; two browser helpers also retried hydration inconsistently. | Applied production matchers and `grepInvert` to all required projects and introduced one shared app-mode hydration retry helper. | `playwright.config.ts`, `playwright-app-mode.ts`, `ui-stress.spec.ts`, `ui-tools.spec.ts` |
| DQA-17 | P2 | Application/tool cards looked like direct links but prevented navigation to open an internal detail state, obscuring the real action. | Cards are now honest dialog buttons with `aria-haspopup="dialog"`; the dialog contains the actual destination link. | `applications-launcher-page.tsx`, `ui-smoke.spec.ts` |
| DQA-18 | P2 | Differential diagnosis streams keyed repeated cards only by visible title, producing React duplicate-key errors and unstable list identity. | Cards now use a stable href/index composite key; the route matrix listens for console errors. | `differential-stream-page.tsx`, `ui-route-coverage.spec.ts` |
| DQA-19 | P2 | The colour-reference skip link targeted `#main-content`, but the route had no matching focus target. | Added the canonical focusable main-content target and browser proof. | `reference/colour-coding/page.tsx`, `ui-route-coverage.spec.ts` |
| DQA-20 | P3 | Answer sheets overrode the semantic backdrop token and duplicate mobile CSS retained obsolete layout overrides. | Removed call-site backdrop overrides and redundant CSS so theme, forced-colour, and responsive behavior come from the shared tokens/layout rules. | `answer-result-surface.tsx`, `globals.css`, `ui-overlay-css-contract.test.ts` |
+| DQA-21 | P2 | Route-handler redirects inherited the production server's `0.0.0.0` bind origin, so Chromium rejected Applications, Medications, and differential-presentation redirects with `ERR_ADDRESS_INVALID`. | Return same-origin relative `Location` headers, preserving allowed query state without leaking the server bind address into browser navigation. | `applications/route.ts`, `medications/route.ts`, `differentials/presentations/route.ts`, route tests |
## Route and component interaction proof
-The new provider-free route matrix covers Therapy Compass, DSM home/compare/differentials, Specifier compare/map, differential diagnosis stream/detail, colour reference, Applications and Medications redirects, and both document-source aliases at desktop and 390 px. It asserts visible primary content, no document-level horizontal overflow, a representative action, blocked external/API leakage, and route-specific behavior.
+The new provider-free route matrix covers Therapy Compass, DSM home/compare/differentials, Specifier compare/map, differential diagnosis stream/detail, colour reference, Applications, Medications and differential-presentation redirects, and both document-source aliases at desktop and 390 px. It asserts visible primary content, no document-level horizontal overflow, a representative action, blocked external/API leakage, and route-specific behavior.
Additional focused browser proof covered:
@@ -53,7 +54,7 @@ Additional focused browser proof covered:
- Differential comparison and the differential stream/detail console-error regression.
- Reduced motion, forced colours, 200% zoom, and axe WCAG A/AA in normal and forced-colour modes.
-The Services control test rendered the correct page and retained a screenshot showing the selected rail, working Clear action, and disabled unfinished controls. Its assertion was incorrectly scoped to a nonexistent test ID; that test now uses the semantic `main` root. Per the recovery instruction, it was not launched a third time after two failed attempts (the first was runner/port contention, the second exposed the selector defect).
+The Services control test rendered the correct page and retained a screenshot showing the selected rail and working Clear action. Its assertion was incorrectly scoped to a nonexistent test ID; that test now uses the semantic `main` root. The final integration also preserves current main's functional checklist, confidence-detail, and selected-service comparison controls. Per the recovery instruction, the earlier audit test was not launched a third time after two failed attempts (the first was runner/port contention, the second exposed the selector defect).
The valid `/documents/source` browser case issued the correct destination request but the cold document-detail compile (about 24 seconds plus source render) exceeded its 30-second browser poll. The exact local response contains `NEXT_REDIRECT;replace;/documents/11111111-1111-4111-8111-111111111111?page=2&chunk=safety+plan;307;`. The test allowance is now 60 seconds, but the same browser action was not retried a third time.
@@ -81,7 +82,7 @@ Broad-gate limitations:
## Residual decisions and risk
- External design-target fidelity remains unprovable until an approved Figma file or screenshot set is supplied.
-- Several future product features (service comparison/editing, therapy favourite persistence, differential editing) remain intentionally unavailable. They now present disabled or read-only states instead of false actions.
+- Therapy favourite persistence and differential column editing remain intentionally unavailable. They now present disabled or read-only states instead of false actions; Services comparison/details are functional in the final current-main integration.
- Live authenticated data, provider failure behavior, and production clinical workflows remain outside this offline design audit and require explicit authorization before testing.
- No dependency, API contract, database schema, production data, or environment file was changed.
@@ -92,3 +93,11 @@ Broad-gate limitations:
- Base HEAD: `47b58ddf95826231a6057b400e7f601c403a129d`
- The audit and verification finished before Git handoff; its reviewed diff is carried forward from this isolated worktree onto the final PR branch.
- No deployment or provider-backed call was made during the audit.
+
+## Final PR integration verification (2026-07-18)
+
+- Final branch/worktree: `codex/design-audit-final-pr-20260718` in `C:\Dev\Apps\Database\worktrees\design-audit-final-pr-20260718`.
+- The current-main merge preserved the routed Therapy workspace, functional Services checklist/confidence/comparison controls, and dialog-first tool cards. Stale tests for the removed legacy wrappers/placeholders were reconciled without restoring inaccessible or false UI.
+- The local PR plan passed runtime, changed-file formatting, full ESLint, TypeScript, 309 Vitest files / 2,797 tests, a webpack production build with 1,674 generated pages, client-bundle secret scanning, and 36 offline RAG golden fixtures across 21 suites. The build was rerun narrowly after an empty `NEXT_PUBLIC_DEMO_MODE` test-shell value was removed; no source change was required for that environment-only failure.
+- The exact-branch Chromium run passed 230/237 tests on its first pass. Focused production recovery passed Therapy navigation and all three relative redirect journeys; Services and Tools then passed against the identity-verified provider-free branch server. The guest upload journey was proven in production through the Source Library and exact admin-only notice; its final strict alert locator was narrowed to exclude Next's empty route announcer. A third duplicate production build was not launched under the recovery retry limit, so hosted exact-head UI remains the final confirmation for that selector.
+- No OpenAI, Supabase, production-data, deployment, or live clinical workflow was invoked. Browser/API fixtures used the repository's offline environment and blocked external requests.
diff --git a/src/app/applications/route.ts b/src/app/applications/route.ts
index 0f19d2c8c..e934c3103 100644
--- a/src/app/applications/route.ts
+++ b/src/app/applications/route.ts
@@ -1,11 +1,12 @@
-import { type NextRequest, NextResponse } from "next/server";
+import { type NextRequest } from "next/server";
// "Tools" is the canonical name and /tools the canonical route (PT-11). A
// route handler redirects the incoming legacy request before React renders it.
export function GET(request: NextRequest) {
- const destination = request.nextUrl.clone();
- destination.pathname = "/tools";
- return NextResponse.redirect(destination);
+ return new Response(null, {
+ status: 307,
+ headers: { Location: `/tools${request.nextUrl.search}` },
+ });
}
export const HEAD = GET;
diff --git a/src/app/differentials/presentations/route.ts b/src/app/differentials/presentations/route.ts
index 001912f0f..64adff510 100644
--- a/src/app/differentials/presentations/route.ts
+++ b/src/app/differentials/presentations/route.ts
@@ -1,4 +1,4 @@
-import { type NextRequest, NextResponse } from "next/server";
+import { type NextRequest } from "next/server";
import { getPresentationWorkflowSelectionForDiagnosisIds } from "@/lib/differentials";
@@ -9,13 +9,16 @@ export function GET(request: NextRequest) {
.map((id) => id.trim())
.filter(Boolean);
const selection = getPresentationWorkflowSelectionForDiagnosisIds(selectedIds);
- const destination = new URL(
- `/differentials/presentations/${selection?.workflow.id ?? "acute-confusion-encephalopathy"}`,
- request.url,
- );
- if (query) destination.searchParams.set("q", query);
- if (selection?.diagnosisIds.length) destination.searchParams.set("ids", selection.diagnosisIds.join(","));
- return NextResponse.redirect(destination);
+ const destinationParams = new URLSearchParams();
+ if (query) destinationParams.set("q", query);
+ if (selection?.diagnosisIds.length) destinationParams.set("ids", selection.diagnosisIds.join(","));
+ const suffix = destinationParams.size ? `?${destinationParams.toString()}` : "";
+ return new Response(null, {
+ status: 307,
+ headers: {
+ Location: `/differentials/presentations/${selection?.workflow.id ?? "acute-confusion-encephalopathy"}${suffix}`,
+ },
+ });
}
export const HEAD = GET;
diff --git a/src/app/medications/route.ts b/src/app/medications/route.ts
index c7ad52d16..9dee796fb 100644
--- a/src/app/medications/route.ts
+++ b/src/app/medications/route.ts
@@ -1,7 +1,10 @@
-import { type NextRequest, NextResponse } from "next/server";
+import { type NextRequest } from "next/server";
-export function GET(request: NextRequest) {
- return NextResponse.redirect(new URL("/?mode=prescribing", request.url));
+export function GET(_request: NextRequest) {
+ return new Response(null, {
+ status: 307,
+ headers: { Location: "/?mode=prescribing" },
+ });
}
export const HEAD = GET;
diff --git a/src/components/clinical-dashboard/answer-result-surface.tsx b/src/components/clinical-dashboard/answer-result-surface.tsx
index 752d816b3..6051ac0e0 100644
--- a/src/components/clinical-dashboard/answer-result-surface.tsx
+++ b/src/components/clinical-dashboard/answer-result-surface.tsx
@@ -286,7 +286,6 @@ function StagedAnswerResultSurfaceImpl({
closeButtonClassName="inline-flex h-8 w-8 items-center justify-center rounded-full text-[color:var(--text-muted)] transition hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
contentClassName="max-h-[88dvh] bg-[color:var(--surface-raised)] sm:max-h-[min(80dvh,36rem)] sm:max-w-md"
bodyClassName="flex flex-col bg-[color:var(--surface-raised)] px-3 pb-0 pt-2 sm:p-3"
- desktopBackdropClassName="sm:bg-[color:var(--overlay-backdrop)]"
returnFocusRef={clinicalNotesTriggerRef}
portal
>
@@ -318,7 +317,6 @@ function StagedAnswerResultSurfaceImpl({
}
contentClassName="max-h-[88dvh] bg-[color:var(--surface-raised)] sm:max-h-[min(88dvh,44rem)] sm:max-w-3xl"
bodyClassName="bg-[color:var(--surface-raised)] px-3 pb-0 pt-2 sm:p-3"
- desktopBackdropClassName="sm:bg-[color:var(--overlay-backdrop)]"
returnFocusRef={evidenceTriggerRef}
portal
>
@@ -363,7 +361,6 @@ function StagedAnswerResultSurfaceImpl({
closeButtonClassName="inline-flex h-8 w-8 items-center justify-center rounded-full text-[color:var(--text-muted)] transition hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
contentClassName="max-h-[88dvh] bg-[color:var(--surface-raised)] sm:max-h-[min(80dvh,36rem)] sm:max-w-lg"
bodyClassName="flex flex-col bg-[color:var(--surface-raised)] px-3 pb-0 pt-2 sm:p-3"
- desktopBackdropClassName="sm:bg-[color:var(--overlay-backdrop)]"
returnFocusRef={safetyTriggerRef}
portal
>
diff --git a/src/components/differentials/differential-stream-page.tsx b/src/components/differentials/differential-stream-page.tsx
index 9219f8d37..6914cab7d 100644
--- a/src/components/differentials/differential-stream-page.tsx
+++ b/src/components/differentials/differential-stream-page.tsx
@@ -59,7 +59,7 @@ export function DifferentialStreamPage({ stream, query = "" }: DifferentialStrea
Diagnosis-focused differential content
- {copy.cards.map((card, index) => (
+ {copy.cards.map((card) => (
{
new NextRequest("https://clinical-kb.test/applications?q=acute+care&tag=one&tag=two"),
);
expect(applications.status).toBe(307);
- expect(applications.headers.get("location")).toBe("https://clinical-kb.test/tools?q=acute+care&tag=one&tag=two");
+ expect(applications.headers.get("location")).toBe("/tools?q=acute+care&tag=one&tag=two");
const presentations = redirectPresentations(
new NextRequest(
@@ -40,12 +40,12 @@ describe("audit navigation and auth regressions", () => {
);
expect(presentations.status).toBe(307);
expect(presentations.headers.get("location")).toBe(
- "https://clinical-kb.test/differentials/presentations/acute-confusion-encephalopathy?q=acute+confusion&ids=delirium",
+ "/differentials/presentations/acute-confusion-encephalopathy?q=acute+confusion&ids=delirium",
);
const medications = redirectMedications(new NextRequest("https://clinical-kb.test/medications?ignored=1"));
expect(medications.status).toBe(307);
- expect(medications.headers.get("location")).toBe("https://clinical-kb.test/?mode=prescribing");
+ expect(medications.headers.get("location")).toBe("/?mode=prescribing");
expect([headApplications, headPresentations, headMedications]).toEqual([
redirectApplications,
@@ -125,6 +125,22 @@ describe("audit navigation and auth regressions", () => {
expect(uploadMutationContract).toContain("if (!canUsePrivateApis) {");
});
+ it("keeps the private upload workspace tabs and panels programmatically associated", () => {
+ expect(clinicalDashboardSource).toContain('aria-label="Upload and indexing sections"');
+ expect(clinicalDashboardSource).toContain('role="tab"');
+ expect(clinicalDashboardSource).toContain("aria-selected={active}");
+ expect(clinicalDashboardSource).toContain("aria-controls={tab.panelId}");
+ expect(clinicalDashboardSource).toContain("tabIndex={active ? 0 : -1}");
+ expect(clinicalDashboardSource).toContain('role="tabpanel"');
+ for (const tab of ["setup", "upload", "jobs", "quality"]) {
+ expect(clinicalDashboardSource).toContain(`aria-labelledby="dashboard-upload-tab-${tab}"`);
+ }
+ expect(clinicalDashboardSource).toContain('event.key === "ArrowRight"');
+ expect(clinicalDashboardSource).toContain('event.key === "ArrowLeft"');
+ expect(clinicalDashboardSource).toContain('event.key === "Home"');
+ expect(clinicalDashboardSource).toContain('event.key === "End"');
+ });
+
it("keeps the root dashboard H1 as Clinical Guide", () => {
expect(clinicalDashboardSource.match(/
\s*Clinical Guide\s*<\/h1>/);
diff --git a/tests/therapy-compass-responsive-contract.test.ts b/tests/therapy-compass-responsive-contract.test.ts
index 534b1b993..ee677d6ad 100644
--- a/tests/therapy-compass-responsive-contract.test.ts
+++ b/tests/therapy-compass-responsive-contract.test.ts
@@ -20,6 +20,10 @@ function classCount(source: string, className: string) {
return source.match(new RegExp(`className="[^"]*\\b${className}\\b[^"]*"`, "g"))?.length ?? 0;
}
+function responsiveStackCount(source: string) {
+ return classCount(source, "tc-mobile-stack") + classCount(source, "tc-stack-sm");
+}
+
function contrastRatio(firstHex: string, secondHex: string) {
const luminance = (hex: string) => {
const channels = hex
@@ -46,23 +50,23 @@ describe("Therapy Compass responsive contract", () => {
});
it("marks every fixed screen/card grid for phone reflow without changing its desktop template", () => {
- expect(classCount(therapyCardSource, "tc-mobile-stack")).toBeGreaterThanOrEqual(2);
- expect(classCount(homeSource, "tc-mobile-stack")).toBeGreaterThanOrEqual(3);
+ expect(responsiveStackCount(therapyCardSource)).toBeGreaterThanOrEqual(2);
+ expect(responsiveStackCount(homeSource)).toBeGreaterThanOrEqual(3);
expect(homeSource).toContain('className="tc-home-search"');
expect(homeSource).toContain("tc-home-search-button");
- expect(classCount(detailSource, "tc-mobile-stack")).toBeGreaterThanOrEqual(2);
+ expect(responsiveStackCount(detailSource)).toBeGreaterThanOrEqual(2);
expect(detailSource).toContain('className="tc-mobile-static"');
- expect(compareSource).toContain('className="tc-mobile-stack"');
+ expect(responsiveStackCount(compareSource)).toBeGreaterThanOrEqual(1);
expect(compareSource).toContain('className="tc-compare-tabs"');
- expect(compareSource).toContain('className="tc-compare-table tc-scroll"');
- expect(classCount(recommendSource, "tc-mobile-stack")).toBeGreaterThanOrEqual(2);
- expect(pathwaysSource).toContain('className="tc-mobile-stack"');
+ expect(compareSource).toMatch(/className="(?:tc-compare-table tc-scroll|tc-scroll-sm)"/);
+ expect(responsiveStackCount(recommendSource)).toBeGreaterThanOrEqual(2);
+ expect(responsiveStackCount(pathwaysSource)).toBeGreaterThanOrEqual(1);
expect(pathwaysSource).toContain('className="tc-pathway-list"');
- expect(classCount(briefSource, "tc-mobile-stack")).toBeGreaterThanOrEqual(2);
+ expect(responsiveStackCount(briefSource)).toBeGreaterThanOrEqual(2);
expect(briefSource).toContain('className="tc-mobile-grid-2"');
- expect(sheetsSource).toContain('className="tc-mobile-stack"');
+ expect(responsiveStackCount(sheetsSource)).toBeGreaterThanOrEqual(1);
expect(sheetsSource).toContain("tc-builder-panel tc-mobile-static");
- expect(otherSource).toContain('className="tc-mobile-stack"');
+ expect(responsiveStackCount(otherSource)).toBeGreaterThanOrEqual(1);
expect(therapyCardSource).toContain("grid-template-columns:minmax(280px,1fr) minmax(400px,1.35fr) auto");
expect(detailSource).toContain("grid-template-columns:minmax(0,1fr) 344px");
diff --git a/tests/ui-accessibility.spec.ts b/tests/ui-accessibility.spec.ts
index 5deada490..3253ec31d 100644
--- a/tests/ui-accessibility.spec.ts
+++ b/tests/ui-accessibility.spec.ts
@@ -318,7 +318,7 @@ test.describe("Clinical KB accessibility coverage", () => {
await expect(presentationsFilter).toHaveAttribute("aria-pressed", "false");
});
- test("upload and indexing tabs expose panel associations and roving keyboard activation", async ({ page }) => {
+ test("guest upload action exposes the admin boundary and opens the source library", async ({ page }) => {
await page.setViewportSize({ width: 414, height: 820 });
await mockMinimalDashboardApi(page);
await gotoApp(page);
@@ -326,52 +326,10 @@ test.describe("Clinical KB accessibility coverage", () => {
const uploadButton = page.getByRole("button", { name: /Upload document/i });
await expect(uploadButton).toBeVisible();
await uploadButton.click();
- const drawer = page.getByRole("dialog", { name: "Upload and indexing" });
- await expect(drawer).toBeVisible();
-
- const tablist = drawer.getByRole("tablist", { name: "Upload and indexing sections" });
- const setupTab = tablist.getByRole("tab", { name: "Setup", exact: true });
- const uploadTab = tablist.getByRole("tab", { name: "Upload", exact: true });
- const jobsTab = tablist.getByRole("tab", { name: "Jobs", exact: true });
- const qualityTab = tablist.getByRole("tab", { name: "Quality", exact: true });
- const associations = [
- [setupTab, "dashboard-setup-section"],
- [uploadTab, "dashboard-upload-section"],
- [jobsTab, "dashboard-indexing-section"],
- [qualityTab, "dashboard-quality-section"],
- ] as const;
-
- for (const [tab, panelId] of associations) {
- const tabId = await tab.getAttribute("id");
- expect(tabId).toBeTruthy();
- await expect(tab).toHaveAttribute("aria-controls", panelId);
- await expect(drawer.locator(`#${panelId}`)).toHaveAttribute("aria-labelledby", tabId!);
- }
-
- await expect(uploadTab).toHaveAttribute("aria-selected", "true");
- await expect(uploadTab).toHaveAttribute("tabindex", "0");
- await expect(jobsTab).toHaveAttribute("tabindex", "-1");
- await uploadTab.focus();
-
- await page.keyboard.press("ArrowRight");
- await expect(jobsTab).toBeFocused();
- await expect(jobsTab).toHaveAttribute("aria-selected", "true");
- await expect(drawer.locator("#dashboard-indexing-section")).toBeVisible();
-
- await page.keyboard.press("ArrowLeft");
- await expect(uploadTab).toBeFocused();
- await expect(uploadTab).toHaveAttribute("aria-selected", "true");
-
- await page.keyboard.press("Home");
- await expect(setupTab).toBeFocused();
- await expect(setupTab).toHaveAttribute("aria-selected", "true");
-
- await page.keyboard.press("End");
- await expect(qualityTab).toBeFocused();
- await expect(qualityTab).toHaveAttribute("aria-selected", "true");
-
- await page.keyboard.press("ArrowRight");
- await expect(setupTab).toBeFocused();
- await expect(setupTab).toHaveAttribute("aria-selected", "true");
+ await expect(page.getByRole("dialog", { name: "Upload and indexing" })).toHaveCount(0);
+ await expect(page.getByRole("dialog", { name: "Source library" })).toBeVisible();
+ await expect(page.getByRole("alert").filter({ hasText: "Upload and indexing tools are admin-only" })).toContainText(
+ "Upload and indexing tools are admin-only. Use the source library to open indexed documents.",
+ );
});
});
diff --git a/tests/ui-route-coverage.spec.ts b/tests/ui-route-coverage.spec.ts
index fe5826cc0..74b0276a6 100644
--- a/tests/ui-route-coverage.spec.ts
+++ b/tests/ui-route-coverage.spec.ts
@@ -224,7 +224,7 @@ test.describe("previously uncovered production routes", () => {
},
async (currentPage) => {
const search = currentPage
- .getByRole("navigation", { name: "Therapy Compass sections" })
+ .getByRole("navigation", { name: "Therapy sections" })
.getByRole("button", { name: "Search", exact: true });
await expect(search).toBeEnabled();
await search.click();
diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts
index 88f8132b6..e84f9c199 100644
--- a/tests/ui-smoke.spec.ts
+++ b/tests/ui-smoke.spec.ts
@@ -2695,29 +2695,6 @@ test.describe("Clinical KB UI smoke coverage", () => {
await expect(hub.getByRole("button", { name: "More actions for Acamprosate renal screen" })).toBeVisible();
});
- test("mobile favourite cards keep selection and actions as independent touch controls", async ({ page }) => {
- await page.setViewportSize({ width: 390, height: 820 });
- await mockDemoApi(page);
- await gotoApp(page, "/favourites");
-
- const card = page.getByTestId("favourite-mobile-card-acamprosate-renal-screen");
- const selectCard = card.getByRole("button", { name: "Select Acamprosate renal screen" });
- const openItem = card.getByRole("link", { name: "Open", exact: true });
- const moreActions = card.getByRole("button", { name: "More actions for Acamprosate renal screen" });
-
- await expect(card).toBeVisible();
- await expect(selectCard).toHaveAttribute("aria-pressed", "false");
- await expectMinTouchTarget(selectCard);
- await expectMinTouchTarget(openItem);
- await expectMinTouchTarget(moreActions);
-
- await selectCard.click();
- await expect(selectCard).toHaveAttribute("aria-pressed", "true");
- await expect(openItem).toBeVisible();
- await expect(moreActions).toBeVisible();
- await expectNoPageHorizontalOverflow(page);
- });
-
test("app mode menu supports keyboard navigation without removed prototype modes", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
await mockDemoApi(page);
@@ -2971,20 +2948,28 @@ test.describe("Clinical KB UI smoke coverage", () => {
await expectNoPageHorizontalOverflow(page);
});
- test("services decision rail does not present unfinished actions as available", async ({ page }) => {
+ test("services decision rail exposes only functional review and comparison actions", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await mockDemoApi(page);
await gotoApp(page, "/services?q=mental%20health&focus=1&run=1");
const navigator = page.getByRole("main");
await expect(navigator).toBeVisible();
- await expect(navigator.getByRole("button", { name: "Edit" })).toBeDisabled();
- await expect(navigator.getByRole("button", { name: "Review details" })).toBeDisabled();
- await expect(navigator.getByRole("button", { name: "View details" })).toBeDisabled();
+ await expect(navigator.getByRole("button", { name: "Edit" })).toHaveCount(0);
+ const reviewDetails = navigator.getByRole("button", { name: "Review details" });
+ await expect(reviewDetails).toBeEnabled();
+ await reviewDetails.click();
+ await expect(navigator.locator("#service-checklist-details")).toBeVisible();
+ const viewDetails = navigator.getByRole("button", { name: "View details" });
+ await expect(viewDetails).toBeEnabled();
+ await viewDetails.click();
+ await expect(navigator.locator("#service-confidence-details")).toBeVisible();
const compare = navigator.getByRole("button", { name: /Compare selected/ });
- await expect(compare).toBeDisabled();
- await expect(compare).toHaveAttribute("title", "Service comparison is coming soon");
- const clear = navigator.getByRole("button", { name: "Clear" });
+ await expect(compare).toBeEnabled();
+ await expect(compare).toHaveAttribute("title", "Compare selected services");
+ await compare.click();
+ await expect(navigator.getByRole("region", { name: "Selected service comparison" })).toBeVisible();
+ const clear = navigator.getByRole("button", { name: "Clear", exact: true });
await expect(clear).toBeEnabled();
await clear.click();
await expect(navigator.getByText("Selected services (0)")).toBeVisible();
diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts
index af3b55940..c5f8cbdf6 100644
--- a/tests/ui-tools.spec.ts
+++ b/tests/ui-tools.spec.ts
@@ -449,14 +449,17 @@ test.describe("Clinical KB tools launcher", () => {
await expect(toolsHub.getByRole("heading", { level: 1, name: "Tools" })).toBeVisible();
await expect(toolsHub.getByTestId("global-search-input")).toBeVisible();
await expect(toolsHub.getByRole("heading", { name: "All tools" })).toBeVisible();
- await expect(toolsHub.getByRole("link", { name: "Launch Medication Prescribing" })).toBeVisible();
+ const medicationDetails = toolsHub.getByRole("button", { name: "View details for Medication Prescribing" });
+ await expect(medicationDetails).toHaveAttribute("aria-haspopup", "dialog");
await expect(toolsHub.getByTestId("application-card-documents")).toBeHidden();
await expect(toolsHub.getByTestId("tool-mode-result-medications")).toHaveCount(0);
- await expect(toolsHub.getByRole("link", { name: "Launch Medication Prescribing" })).toHaveAttribute(
- "href",
- "/?mode=prescribing",
- );
+ await medicationDetails.click();
+ const medicationDialog = page.getByRole("dialog", { name: "Medication Prescribing" });
+ await expect(medicationDialog).toBeVisible();
+ const medicationLaunch = medicationDialog.locator('a[href="/?mode=prescribing"]').first();
+ await expect(medicationLaunch).toBeVisible();
+ await expect(medicationLaunch).toHaveAttribute("href", "/?mode=prescribing");
await expectNoPageHorizontalOverflow(page);
});
From 8071a6704df55375125d87dfd35e83723fa19452 Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Sat, 18 Jul 2026 22:54:24 +0800
Subject: [PATCH 03/21] docs: record design audit PR readiness
---
docs/branch-review-ledger.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md
index 06d64eb6c..b41423089 100644
--- a/docs/branch-review-ledger.md
+++ b/docs/branch-review-ledger.md
@@ -606,3 +606,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-17 | codex/design-audit-20260716 | 47b58ddf95826231a6057b400e7f601c403a129d + reviewed working diff | exhaustive design, accessibility, UX, interaction, route, responsive, HTML/CSS/JS/TSX, and test-coverage audit with remediation | No P0. Fixed two P1 trust/responsive defects and the confirmed P2/P3 set: false Favourites provenance/demo leakage/dead controls, Therapy phone reflow/load recovery/contrast, semantic tabs/search/filters, error focus/theme, deterministic Forms back navigation, honest unavailable actions, application dialog semantics, duplicate differential keys, skip-link target, Playwright project isolation, and route coverage. External visual-target fidelity remains unavailable. | Static 261-file/~67,944-line inventory; independent combined-diff review; focused Vitest 21/21; jsdom 24/24; architecture 6/6; accessibility browser 5/5; focused route/browser recovery; lint; TypeScript; runtime/workflow/sitemap/brand/type/icon checks; production build with 1,043 pages and client-secret scan; `git diff --check`. Canonical aggregate Vitest was Windows-runner inconclusive and full UI was not repeated after focused remediation. No provider/API checks. |
| 2026-07-18 | codex/main-merge-51278-final-20260718-late | 45fa3c6c7, 14a0a898c | merge integration of 51278a70d onto fresh origin/main | Replayed the requested historical design-audit commit onto fresh `origin/main`. Kept current-main versions for five conflicts, including the regenerated drift manifest and current schema assertions. Fixed the duplicate sitemap-generator declaration, corrected redirect-section coverage, and restored the current `Clinical Guide` UI contract. | `git diff --check` and `npm run sitemap:check` passed. `npm run test` was attempted twice but blocked by the repository-wide heavy-command lock held by a separate Playwright worktree; no provider-backed checks ran. |
| 2026-07-18 | origin/main framework and dependency modernization snapshot | 4057677c8b92a5e1d997ec44958764fa91f5d424 | parallel build/infra, backend, and frontend modernization audit | Changes requested. No P0. Confirmed two P1 defects: Supabase SSR 0.12 auth-cookie responses discard mandatory anti-cache headers, and reindex bypasses the server-only-aware TSX runner after mutation-capable setup. Five P2 blockers cover the Webpack-to-Turbopack production cutover, incomplete Railway image-build watch ownership, missing clean `next typegen`, Node 26 types over a Node 24 runtime, and App Router retry actions that reset without re-fetching. P3 removal-readiness debt remains in Zod and Next Image APIs. Manual rewrite zones are auth response ownership, bundler/CSP/artifact consumers, JSZip resource limits, OpenAI request typing, and React Compiler adoption in the stateful dashboard/viewer roots. | Parallel read-only source/config/test audit against the exact snapshot; Node 24.18.0/npm 11.17.0; exact-version Next 16.2.10 bundled upgrade, Turbopack, error-boundary, and codemod guidance; TypeScript 6 backend no-emit analysis and Node import probes via a separately installed exact-version local dependency tree; `git diff --check`. No install, registry outdated/audit, full repo lint/typecheck/test/build/browser/Docker, Supabase/OpenAI, deployment, or hosted CI run; this worktree had no `node_modules`. |
+| 2026-07-18 | codex/design-audit-final-pr-20260718 | 9378743c15fed7d5097871de53079e6c0b6b202b + final ledger-only commit | final current-main design/accessibility audit integration and PR-readiness review | No remaining high-confidence P0-P2 defect in the 53-file PR diff after current-main reconciliation and an independent read-only review. Fixed stale mobile Favourites, Therapy, Services, Tools, upload-boundary, and overlay expectations without restoring obsolete controls; removed merge residue; and fixed three same-origin route redirects that leaked the `0.0.0.0` server bind address into Chromium. External design-target fidelity remains unverified without an approved target. | Pre-sync local PR plan: runtime, formatting, full ESLint, TypeScript, 309 files / 2,797 Vitest tests, webpack production build with 1,674 pages and client-secret scan, and 36 offline RAG fixtures across 21 suites. Chromium: 230/237 initial exact-branch pass; four corrected production cases; Services and Tools selector recovery passed against the verified offline branch server; production guest-upload evidence reached the correct Source Library and notice before a narrowed strict locator. Post-sync: focused Vitest 23/23, TypeScript, format, and diff checks passed. Exact-head hosted UI remains the final selector confirmation. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. |
From c5e7b96c3c8912c684878515260f1bf0e56584eb Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Sat, 18 Jul 2026 23:04:43 +0800
Subject: [PATCH 04/21] fix: satisfy strict redirect lint
---
src/app/medications/route.ts | 4 +---
tests/audit-navigation-auth-regressions.test.ts | 2 +-
2 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/src/app/medications/route.ts b/src/app/medications/route.ts
index 9dee796fb..5ebeac9a8 100644
--- a/src/app/medications/route.ts
+++ b/src/app/medications/route.ts
@@ -1,6 +1,4 @@
-import { type NextRequest } from "next/server";
-
-export function GET(_request: NextRequest) {
+export function GET() {
return new Response(null, {
status: 307,
headers: { Location: "/?mode=prescribing" },
diff --git a/tests/audit-navigation-auth-regressions.test.ts b/tests/audit-navigation-auth-regressions.test.ts
index 11c8d83e8..73973d114 100644
--- a/tests/audit-navigation-auth-regressions.test.ts
+++ b/tests/audit-navigation-auth-regressions.test.ts
@@ -43,7 +43,7 @@ describe("audit navigation and auth regressions", () => {
"/differentials/presentations/acute-confusion-encephalopathy?q=acute+confusion&ids=delirium",
);
- const medications = redirectMedications(new NextRequest("https://clinical-kb.test/medications?ignored=1"));
+ const medications = redirectMedications();
expect(medications.status).toBe(307);
expect(medications.headers.get("location")).toBe("/?mode=prescribing");
From 77142188d598683a13c5f6c14b25eece1679bef0 Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Sat, 18 Jul 2026 23:14:12 +0800
Subject: [PATCH 05/21] fix: resolve design audit review findings
---
docs/branch-review-ledger.md | 2 +-
docs/design-audit-2026-07-17.md | 46 +++++++++----------
src/app/reference/colour-coding/page.tsx | 9 +++-
src/components/ClinicalDashboard.tsx | 13 ++++--
.../clinical-dashboard/favourites-hub.tsx | 33 +++++++++----
.../global-search-shell.tsx | 16 +++++--
src/lib/client-env.ts | 14 ++++++
tests/favourites-demo-boundary.test.ts | 34 ++++++++++++--
...ites-hub-unavailable-controls.dom.test.tsx | 33 +++++++++++++
tests/ui-route-coverage.spec.ts | 6 ++-
10 files changed, 159 insertions(+), 47 deletions(-)
create mode 100644 tests/favourites-hub-unavailable-controls.dom.test.tsx
diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md
index b41423089..ea8b69514 100644
--- a/docs/branch-review-ledger.md
+++ b/docs/branch-review-ledger.md
@@ -606,4 +606,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-17 | codex/design-audit-20260716 | 47b58ddf95826231a6057b400e7f601c403a129d + reviewed working diff | exhaustive design, accessibility, UX, interaction, route, responsive, HTML/CSS/JS/TSX, and test-coverage audit with remediation | No P0. Fixed two P1 trust/responsive defects and the confirmed P2/P3 set: false Favourites provenance/demo leakage/dead controls, Therapy phone reflow/load recovery/contrast, semantic tabs/search/filters, error focus/theme, deterministic Forms back navigation, honest unavailable actions, application dialog semantics, duplicate differential keys, skip-link target, Playwright project isolation, and route coverage. External visual-target fidelity remains unavailable. | Static 261-file/~67,944-line inventory; independent combined-diff review; focused Vitest 21/21; jsdom 24/24; architecture 6/6; accessibility browser 5/5; focused route/browser recovery; lint; TypeScript; runtime/workflow/sitemap/brand/type/icon checks; production build with 1,043 pages and client-secret scan; `git diff --check`. Canonical aggregate Vitest was Windows-runner inconclusive and full UI was not repeated after focused remediation. No provider/API checks. |
| 2026-07-18 | codex/main-merge-51278-final-20260718-late | 45fa3c6c7, 14a0a898c | merge integration of 51278a70d onto fresh origin/main | Replayed the requested historical design-audit commit onto fresh `origin/main`. Kept current-main versions for five conflicts, including the regenerated drift manifest and current schema assertions. Fixed the duplicate sitemap-generator declaration, corrected redirect-section coverage, and restored the current `Clinical Guide` UI contract. | `git diff --check` and `npm run sitemap:check` passed. `npm run test` was attempted twice but blocked by the repository-wide heavy-command lock held by a separate Playwright worktree; no provider-backed checks ran. |
| 2026-07-18 | origin/main framework and dependency modernization snapshot | 4057677c8b92a5e1d997ec44958764fa91f5d424 | parallel build/infra, backend, and frontend modernization audit | Changes requested. No P0. Confirmed two P1 defects: Supabase SSR 0.12 auth-cookie responses discard mandatory anti-cache headers, and reindex bypasses the server-only-aware TSX runner after mutation-capable setup. Five P2 blockers cover the Webpack-to-Turbopack production cutover, incomplete Railway image-build watch ownership, missing clean `next typegen`, Node 26 types over a Node 24 runtime, and App Router retry actions that reset without re-fetching. P3 removal-readiness debt remains in Zod and Next Image APIs. Manual rewrite zones are auth response ownership, bundler/CSP/artifact consumers, JSZip resource limits, OpenAI request typing, and React Compiler adoption in the stateful dashboard/viewer roots. | Parallel read-only source/config/test audit against the exact snapshot; Node 24.18.0/npm 11.17.0; exact-version Next 16.2.10 bundled upgrade, Turbopack, error-boundary, and codemod guidance; TypeScript 6 backend no-emit analysis and Node import probes via a separately installed exact-version local dependency tree; `git diff --check`. No install, registry outdated/audit, full repo lint/typecheck/test/build/browser/Docker, Supabase/OpenAI, deployment, or hosted CI run; this worktree had no `node_modules`. |
-| 2026-07-18 | codex/design-audit-final-pr-20260718 | 9378743c15fed7d5097871de53079e6c0b6b202b + final ledger-only commit | final current-main design/accessibility audit integration and PR-readiness review | No remaining high-confidence P0-P2 defect in the 53-file PR diff after current-main reconciliation and an independent read-only review. Fixed stale mobile Favourites, Therapy, Services, Tools, upload-boundary, and overlay expectations without restoring obsolete controls; removed merge residue; and fixed three same-origin route redirects that leaked the `0.0.0.0` server bind address into Chromium. External design-target fidelity remains unverified without an approved target. | Pre-sync local PR plan: runtime, formatting, full ESLint, TypeScript, 309 files / 2,797 Vitest tests, webpack production build with 1,674 pages and client-secret scan, and 36 offline RAG fixtures across 21 suites. Chromium: 230/237 initial exact-branch pass; four corrected production cases; Services and Tools selector recovery passed against the verified offline branch server; production guest-upload evidence reached the correct Source Library and notice before a narrowed strict locator. Post-sync: focused Vitest 23/23, TypeScript, format, and diff checks passed. Exact-head hosted UI remains the final selector confirmation. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. |
+| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | c5e7b96c3 + reviewed follow-up diff | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and disposition of all four CodeRabbit comments. The follow-up made client demo state fail closed in production, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, and removed the sole zero-warning CI lint failure. External design-target fidelity remains unverified without an approved target. | Local PR plan: formatting, full ESLint, TypeScript, 309 files / 2,797 Vitest tests, webpack production build with 1,674 pages and client-secret scan, and 36 offline RAG fixtures across 21 suites. Initial hosted Production UI passed on `8071a6704`; focused review-fix Vitest 3/3 and zero-warning ESLint passed; formatting and diff checks passed. A post-fix local typecheck was deferred because another registered worktree held the heavyweight lock; exact-head hosted static/type/UI checks remain required after push. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. |
diff --git a/docs/design-audit-2026-07-17.md b/docs/design-audit-2026-07-17.md
index 74cddd0fd..2eb0cfde8 100644
--- a/docs/design-audit-2026-07-17.md
+++ b/docs/design-audit-2026-07-17.md
@@ -17,29 +17,29 @@ This pass deliberately built on the 2026-07-15 live design audit instead of repe
## Findings and fixes
-| ID | Severity | Detailed issue and user impact | Implemented solution | Principal files |
-| ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| DQA-01 | P1 | Favourites rendered the same acamprosate evidence and lithium note beneath unrelated selected items, creating false item-level provenance. | Evidence and notes now derive from the selected item; absent content has an explicit honest empty state. Copy actions use the item citation and return accessible status. | `favourites-command-library-page.tsx`, `ui-smoke.spec.ts` |
-| DQA-02 | P1 | Therapy Compass used fixed desktop grid tracks wider than a phone while the global shell clipped page overflow, making several production screens unreachable at 320–390 px. | Added semantic responsive layout classes, one-column phone reflow, wrapping actions/metrics, and a contract test across all Therapy Compass screens. | `therapy-compass/*`, `therapy-compass-responsive-contract.test.ts` |
-| DQA-03 | P2 | White text/icons on the dark clinical accent measured materially below accessible contrast and bypassed the existing contrast token. | Replaced hard-coded white foregrounds with `--clinical-accent-contrast`, including answer status and Therapy Compass controls. | `answer-status.tsx`, `therapy-compass/*` |
-| DQA-04 | P2 | Prototype favourites appeared as genuine saved user content in normal mode, including through universal search. | Gated fixtures behind the effective client demo state on the route, hub, command library, and universal search surface. Normal mode now shows only real saved items. | `favourites/page.tsx`, `ClinicalDashboard.tsx`, `favourites-hub.tsx`, `global-search-shell.tsx`, `master-search-header.tsx`, `universal-search-command-surface.tsx` |
-| DQA-05 | P2 | Favourites exposed invisible selection below `2xl`, nested/synthetic mobile card controls, no-op edit/sort actions, incomplete menu keyboard behavior, and clipboard focus loss. | Kept explicit native links and separate buttons on mobile, limited selection to the visible workspace, implemented real citation copy with status/focus restoration, added complete menu keyboard behavior, and disabled or relabelled unfinished actions honestly. | `favourites-command-library-page.tsx`, `favourites-hub.tsx` |
-| DQA-06 | P2 | Differential presentation controls advertised editing, filtering, reorder/remove behavior, and a fixed `+2` count that did not exist. | Removed false instructions, derived the remaining count from state, and disabled unfinished controls with availability explanations while preserving the read-only comparison. | `differential-presentation-workflow-page.tsx` |
-| DQA-07 | P2 | Services selection led to enabled-looking Edit/Review/View controls that did nothing, while Compare stayed unavailable without an accurate reason. A later diff briefly disabled the real Clear action. | Removed the false actions and restored Clear. During current-main integration, the later functional checklist, confidence-detail, and selected-service comparison controls were preserved instead of replacing them with disabled placeholders. | `services-navigator-page.tsx`, `ui-smoke.spec.ts` |
-| DQA-08 | P2 | Evidence records with no URL were still keyboard-focusable `href="#"` links announced as “Open source”. | Missing-source rows now render as non-links with unavailable semantics; only valid destinations render a Link. | `visual-evidence.tsx`, `visual-evidence-tabs.dom.test.tsx` |
-| DQA-09 | P2 | Therapy cards presented an enabled Favourite button with no handler or state change. | The unavailable feature is now disabled and labelled as coming soon, preventing false save confirmation. | `therapy-card.tsx` |
-| DQA-10 | P2 | Route/global error fallbacks did not reliably move focus or announce a route failure; the global fallback was hard-coded light-only. | Added programmatic heading focus and alert announcement, and made the self-contained global fallback system-colour, dark-mode, and forced-colour aware. | `route-error-boundary.tsx`, `global-error.tsx`, route error tests |
-| DQA-11 | P2 | Clinical Notes, Visual Evidence, and upload/index tab interfaces lacked complete roving keyboard behavior and reciprocal tab/panel relationships. | Added stable unique IDs, `aria-controls`/`aria-labelledby`, roving `tabIndex`, and Left/Right/Home/End auto-activation. | `ClinicalDashboard.tsx`, `answer-result-surface.tsx`, `visual-evidence.tsx`, UI/DOM tests |
-| DQA-12 | P2 | Universal search visually highlighted an item without exposing it through `aria-activedescendant`. | The combobox now points to the active option and tests verify the relationship through keyboard navigation. | `master-search-header.tsx`, `universal-search-command-surface.tsx`, `ui-universal-search.spec.ts` |
-| DQA-13 | P2 | Differential filters and several selector groups behaved as toggles but lacked grouped/pressed semantics. | Added labelled groups and stateful `aria-pressed` semantics, with browser accessibility assertions. | `differentials-home.tsx`, `evidence-panels.tsx`, `ui-accessibility.spec.ts` |
-| DQA-14 | P2 | Therapy Compass silently swallowed data-load failure and still implied a fabricated `200+` catalogue count. | Added an explicit failure state and Retry action, removed fabricated totals, and retained real successful/empty states. The final integration tests the routed workspace that replaced the legacy single-page wrapper. | `use-therapy-data.ts`, `workspace.tsx`, `therapy-compass-data-recovery.dom.test.tsx` |
-| DQA-15 | P2 | Direct-entry Forms “Back to forms” used browser history and could leave the application. | The control now deterministically navigates to `/forms?focus=1`; a DOM regression test verifies the destination. | `form-detail-page.tsx`, `forms-back-navigation.dom.test.tsx` |
-| DQA-16 | P2 | Firefox/WebKit production projects unintentionally collected advisory `@mockup` tests; two browser helpers also retried hydration inconsistently. | Applied production matchers and `grepInvert` to all required projects and introduced one shared app-mode hydration retry helper. | `playwright.config.ts`, `playwright-app-mode.ts`, `ui-stress.spec.ts`, `ui-tools.spec.ts` |
-| DQA-17 | P2 | Application/tool cards looked like direct links but prevented navigation to open an internal detail state, obscuring the real action. | Cards are now honest dialog buttons with `aria-haspopup="dialog"`; the dialog contains the actual destination link. | `applications-launcher-page.tsx`, `ui-smoke.spec.ts` |
-| DQA-18 | P2 | Differential diagnosis streams keyed repeated cards only by visible title, producing React duplicate-key errors and unstable list identity. | Cards now use a stable href/index composite key; the route matrix listens for console errors. | `differential-stream-page.tsx`, `ui-route-coverage.spec.ts` |
-| DQA-19 | P2 | The colour-reference skip link targeted `#main-content`, but the route had no matching focus target. | Added the canonical focusable main-content target and browser proof. | `reference/colour-coding/page.tsx`, `ui-route-coverage.spec.ts` |
-| DQA-20 | P3 | Answer sheets overrode the semantic backdrop token and duplicate mobile CSS retained obsolete layout overrides. | Removed call-site backdrop overrides and redundant CSS so theme, forced-colour, and responsive behavior come from the shared tokens/layout rules. | `answer-result-surface.tsx`, `globals.css`, `ui-overlay-css-contract.test.ts` |
-| DQA-21 | P2 | Route-handler redirects inherited the production server's `0.0.0.0` bind origin, so Chromium rejected Applications, Medications, and differential-presentation redirects with `ERR_ADDRESS_INVALID`. | Return same-origin relative `Location` headers, preserving allowed query state without leaking the server bind address into browser navigation. | `applications/route.ts`, `medications/route.ts`, `differentials/presentations/route.ts`, route tests |
+| ID | Severity | Detailed issue and user impact | Implemented solution | Principal files |
+| ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| DQA-01 | P1 | Favourites rendered the same acamprosate evidence and lithium note beneath unrelated selected items, creating false item-level provenance. | Evidence and notes now derive from the selected item; absent content has an explicit honest empty state. Copy actions use the item citation and return accessible status. | `favourites-command-library-page.tsx`, `ui-smoke.spec.ts` |
+| DQA-02 | P1 | Therapy Compass used fixed desktop grid tracks wider than a phone while the global shell clipped page overflow, making several production screens unreachable at 320–390 px. | Added semantic responsive layout classes, one-column phone reflow, wrapping actions/metrics, and a contract test across all Therapy Compass screens. | `therapy-compass/*`, `therapy-compass-responsive-contract.test.ts` |
+| DQA-03 | P2 | White text/icons on the dark clinical accent measured materially below accessible contrast and bypassed the existing contrast token. | Replaced hard-coded white foregrounds with `--clinical-accent-contrast`, including answer status and Therapy Compass controls. | `answer-status.tsx`, `therapy-compass/*` |
+| DQA-04 | P2 | Prototype favourites appeared as genuine saved user content in normal mode, including through universal search; an unconfigured browser auth client could also fail open to demo state in production. | Gated fixtures behind the effective client demo state and introduced a shared resolver that permits auth-unavailable fallback only outside production while retaining explicit production demo opt-in. Normal mode now shows only real saved items. | `client-env.ts`, `favourites/page.tsx`, `ClinicalDashboard.tsx`, `favourites-hub.tsx`, `global-search-shell.tsx`, `master-search-header.tsx`, `universal-search-command-surface.tsx` |
+| DQA-05 | P2 | Favourites exposed invisible selection below `2xl`, nested/synthetic mobile card controls, no-op edit/sort actions, incomplete menu keyboard behavior, clipboard focus loss, and title-only reasons on unfocusable unavailable controls. | Kept explicit native links and separate buttons on mobile, limited selection to the visible workspace, implemented real citation copy with status/focus restoration and complete menu keyboard behavior, and made unfinished actions honestly `aria-disabled`, focusable, and accessibly described. | `favourites-command-library-page.tsx`, `favourites-hub.tsx`, `favourites-hub-unavailable-controls.dom.test.tsx` |
+| DQA-06 | P2 | Differential presentation controls advertised editing, filtering, reorder/remove behavior, and a fixed `+2` count that did not exist. | Removed false instructions, derived the remaining count from state, and disabled unfinished controls with availability explanations while preserving the read-only comparison. | `differential-presentation-workflow-page.tsx` |
+| DQA-07 | P2 | Services selection led to enabled-looking Edit/Review/View controls that did nothing, while Compare stayed unavailable without an accurate reason. A later diff briefly disabled the real Clear action. | Removed the false actions and restored Clear. During current-main integration, the later functional checklist, confidence-detail, and selected-service comparison controls were preserved instead of replacing them with disabled placeholders. | `services-navigator-page.tsx`, `ui-smoke.spec.ts` |
+| DQA-08 | P2 | Evidence records with no URL were still keyboard-focusable `href="#"` links announced as “Open source”. | Missing-source rows now render as non-links with unavailable semantics; only valid destinations render a Link. | `visual-evidence.tsx`, `visual-evidence-tabs.dom.test.tsx` |
+| DQA-09 | P2 | Therapy cards presented an enabled Favourite button with no handler or state change. | The unavailable feature is now disabled and labelled as coming soon, preventing false save confirmation. | `therapy-card.tsx` |
+| DQA-10 | P2 | Route/global error fallbacks did not reliably move focus or announce a route failure; the global fallback was hard-coded light-only. | Added programmatic heading focus and alert announcement, and made the self-contained global fallback system-colour, dark-mode, and forced-colour aware. | `route-error-boundary.tsx`, `global-error.tsx`, route error tests |
+| DQA-11 | P2 | Clinical Notes, Visual Evidence, and upload/index tab interfaces lacked complete roving keyboard behavior and reciprocal tab/panel relationships. | Added stable unique IDs, `aria-controls`/`aria-labelledby`, roving `tabIndex`, and Left/Right/Home/End auto-activation. | `ClinicalDashboard.tsx`, `answer-result-surface.tsx`, `visual-evidence.tsx`, UI/DOM tests |
+| DQA-12 | P2 | Universal search visually highlighted an item without exposing it through `aria-activedescendant`. | The combobox now points to the active option and tests verify the relationship through keyboard navigation. | `master-search-header.tsx`, `universal-search-command-surface.tsx`, `ui-universal-search.spec.ts` |
+| DQA-13 | P2 | Differential filters and several selector groups behaved as toggles but lacked grouped/pressed semantics. | Added labelled groups and stateful `aria-pressed` semantics, with browser accessibility assertions. | `differentials-home.tsx`, `evidence-panels.tsx`, `ui-accessibility.spec.ts` |
+| DQA-14 | P2 | Therapy Compass silently swallowed data-load failure and still implied a fabricated `200+` catalogue count. | Added an explicit failure state and Retry action, removed fabricated totals, and retained real successful/empty states. The final integration tests the routed workspace that replaced the legacy single-page wrapper. | `use-therapy-data.ts`, `workspace.tsx`, `therapy-compass-data-recovery.dom.test.tsx` |
+| DQA-15 | P2 | Direct-entry Forms “Back to forms” used browser history and could leave the application. | The control now deterministically navigates to `/forms?focus=1`; a DOM regression test verifies the destination. | `form-detail-page.tsx`, `forms-back-navigation.dom.test.tsx` |
+| DQA-16 | P2 | Firefox/WebKit production projects unintentionally collected advisory `@mockup` tests; two browser helpers also retried hydration inconsistently. | Applied production matchers and `grepInvert` to all required projects and introduced one shared app-mode hydration retry helper. | `playwright.config.ts`, `playwright-app-mode.ts`, `ui-stress.spec.ts`, `ui-tools.spec.ts` |
+| DQA-17 | P2 | Application/tool cards looked like direct links but prevented navigation to open an internal detail state, obscuring the real action. | Cards are now honest dialog buttons with `aria-haspopup="dialog"`; the dialog contains the actual destination link. | `applications-launcher-page.tsx`, `ui-smoke.spec.ts` |
+| DQA-18 | P2 | Differential diagnosis streams keyed repeated cards only by visible title, producing React duplicate-key errors and unstable list identity. | Cards now use a stable href/index composite key; the route matrix listens for console errors. | `differential-stream-page.tsx`, `ui-route-coverage.spec.ts` |
+| DQA-19 | P2 | The colour-reference skip link targeted `#main-content`, but the route had no matching focus target; app-shell targets also suppressed their focus outline. | Added canonical focusable main-content targets with an explicit visible focus outline and browser proof of both focus transfer and computed outline style. | `reference/colour-coding/page.tsx`, `ClinicalDashboard.tsx`, `global-search-shell.tsx`, `ui-route-coverage.spec.ts` |
+| DQA-20 | P3 | Answer sheets overrode the semantic backdrop token and duplicate mobile CSS retained obsolete layout overrides. | Removed call-site backdrop overrides and redundant CSS so theme, forced-colour, and responsive behavior come from the shared tokens/layout rules. | `answer-result-surface.tsx`, `globals.css`, `ui-overlay-css-contract.test.ts` |
+| DQA-21 | P2 | Route-handler redirects inherited the production server's `0.0.0.0` bind origin, so Chromium rejected Applications, Medications, and differential-presentation redirects with `ERR_ADDRESS_INVALID`. | Return same-origin relative `Location` headers, preserving allowed query state without leaking the server bind address into browser navigation. | `applications/route.ts`, `medications/route.ts`, `differentials/presentations/route.ts`, route tests |
## Route and component interaction proof
diff --git a/src/app/reference/colour-coding/page.tsx b/src/app/reference/colour-coding/page.tsx
index 08eed8b00..d5e0a946e 100644
--- a/src/app/reference/colour-coding/page.tsx
+++ b/src/app/reference/colour-coding/page.tsx
@@ -31,7 +31,14 @@ const TONE_SAMPLE_LABEL: Record<(typeof SEMANTIC_TONES)[number], string> = {
export default function ColourCodingReferencePage() {
return (
-
+
diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx
index aa07eb02d..643532a72 100644
--- a/src/components/ClinicalDashboard.tsx
+++ b/src/components/ClinicalDashboard.tsx
@@ -35,7 +35,7 @@ import {
} from "react";
import { type DocumentDeleteResult } from "@/components/DocumentManagementActions";
import { extractSafetyFindings } from "@/lib/clinical-safety";
-import { isLocalNoAuthMode, publicUploadsEnabled } from "@/lib/client-env";
+import { isLocalNoAuthMode, publicUploadsEnabled, resolveClientDemoMode } from "@/lib/client-env";
import { readLocalProjectIdentity, unsafeLocalProjectMessage } from "@/lib/local-project-identity";
import { isDeployedClinicalKb } from "@/lib/deployed-app";
import {
@@ -782,7 +782,11 @@ export function ClinicalDashboard({
const browserAuthUnavailableDemoFallback = !auth.isConfigured && supabaseEnvStatus !== "ready";
const localNoAuthMode = isLocalNoAuthMode();
const explicitDemoMode = demoMode || process.env.NEXT_PUBLIC_DEMO_MODE === "true";
- const clientDemoMode = explicitDemoMode || browserAuthUnavailableDemoFallback || localNoAuthMode;
+ const clientDemoMode = resolveClientDemoMode({
+ explicitDemoMode,
+ authUnavailableFallback: browserAuthUnavailableDemoFallback,
+ localNoAuthMode,
+ });
const answerThreadOwnerId = auth.session?.user.id ?? (clientDemoMode ? demoRecentQueryOwnerId : null);
const previousAnswerThreadOwnerIdRef = useRef(answerThreadOwnerId);
useEffect(() => {
@@ -834,8 +838,7 @@ export function ClinicalDashboard({
setAnswerThreadBootstrapped(true);
});
}, [answerThreadOwnerId, authStatus]);
- const uploadReadOnlyMode =
- demoMode || process.env.NEXT_PUBLIC_DEMO_MODE === "true" || browserAuthUnavailableDemoFallback;
+ const uploadReadOnlyMode = clientDemoMode;
const localDevCanAttemptPrivateApis = process.env.NODE_ENV !== "production" && hasReadyPublicSearchSetup(setupChecks);
const canUsePublicSearchApis = localProjectReady && hasReadyPublicSearchSetup(setupChecks);
const canUseDegradedLocalSearchApis =
@@ -3491,7 +3494,7 @@ export function ClinicalDashboard({
tabIndex={-1}
onScroll={handleMainScroll}
className={cn(
- "min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain [-webkit-overflow-scrolling:touch] focus:outline-none",
+ "min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain [-webkit-overflow-scrolling:touch] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--focus)]",
// Answer view: the glass header is absolute over this scroll container,
// so reserves its exact height as top padding (72px borderless
// bar = 4rem content/padding + the max(0.5rem, safe-area) top inset —
diff --git a/src/components/clinical-dashboard/favourites-hub.tsx b/src/components/clinical-dashboard/favourites-hub.tsx
index e411c4931..93efa33d1 100644
--- a/src/components/clinical-dashboard/favourites-hub.tsx
+++ b/src/components/clinical-dashboard/favourites-hub.tsx
@@ -307,26 +307,35 @@ export function FavouritesHub({
Recent
+
+ Additional sort options are coming soon.
+
Add favourite
Add
+
+ Adding favourites from this screen is coming soon.
+
@@ -445,13 +454,19 @@ export function FavouritesHub({
New set
+
+ Creating favourite sets is coming soon.
+
diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx
index 14e631659..d1b0469c1 100644
--- a/src/components/clinical-dashboard/global-search-shell.tsx
+++ b/src/components/clinical-dashboard/global-search-shell.tsx
@@ -43,7 +43,7 @@ import {
visibleAppModeDefinitions,
type AppModeId,
} from "@/lib/app-modes";
-import { isLocalNoAuthMode } from "@/lib/client-env";
+import { isLocalNoAuthMode, resolveClientDemoMode } from "@/lib/client-env";
import { documentsSearchHref } from "@/lib/document-flow-routes";
import { differentialsMobileCompareAddonSlotId, modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";
import { readSearchNavigationContext, type SearchNavigationOptions } from "@/lib/search-navigation-context";
@@ -314,7 +314,11 @@ function GlobalStandaloneSearchShellClient({
// Recent queries are owner-scoped session state (2026-07-13 audit, finding 4):
// the legacy unscoped localStorage value could resurface another account's
// clinical queries on a shared workstation, so it is deleted, never read.
- const clientDemoMode = !auth.isConfigured || process.env.NEXT_PUBLIC_DEMO_MODE === "true" || isLocalNoAuthMode();
+ const clientDemoMode = resolveClientDemoMode({
+ explicitDemoMode: process.env.NEXT_PUBLIC_DEMO_MODE === "true",
+ authUnavailableFallback: !auth.isConfigured,
+ localNoAuthMode: isLocalNoAuthMode(),
+ });
const recentQueriesOwnerId = auth.session?.user.id ?? (clientDemoMode ? demoRecentQueryOwnerId : null);
useEffect(() => {
@@ -453,7 +457,11 @@ function GlobalStandaloneSearchShellClient({
if (!chromeVisible) {
return (
-
@@ -579,7 +587,7 @@ function GlobalStandaloneSearchShellClient({
// auto, which turns #main-content into the sticky scrollport while the
// window does the actual scrolling — silently disabling every
// position:sticky descendant (e.g. the document viewer rail).
- "min-w-0 focus:outline-none max-sm:flex max-sm:min-h-0 max-sm:flex-1 max-sm:flex-col max-sm:overflow-x-hidden max-sm:overflow-y-auto max-sm:overscroll-contain max-sm:[-webkit-overflow-scrolling:touch] sm:min-h-[calc(100dvh-4rem)] sm:overflow-x-clip",
+ "min-w-0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--focus)] max-sm:flex max-sm:min-h-0 max-sm:flex-1 max-sm:flex-col max-sm:overflow-x-hidden max-sm:overflow-y-auto max-sm:overscroll-contain max-sm:[-webkit-overflow-scrolling:touch] sm:min-h-[calc(100dvh-4rem)] sm:overflow-x-clip",
!reservesFloatingComposer
? "max-sm:pb-[var(--mobile-composer-reserve)] sm:pb-8"
: searchMode === "answer"
diff --git a/src/lib/client-env.ts b/src/lib/client-env.ts
index a23d5a66d..27009bae6 100644
--- a/src/lib/client-env.ts
+++ b/src/lib/client-env.ts
@@ -3,6 +3,20 @@ export function isLocalNoAuthMode() {
return process.env.NODE_ENV !== "production" && process.env.NEXT_PUBLIC_LOCAL_NO_AUTH === "true";
}
+export function resolveClientDemoMode({
+ explicitDemoMode,
+ authUnavailableFallback,
+ localNoAuthMode,
+ environment = process.env.NODE_ENV,
+}: {
+ explicitDemoMode: boolean;
+ authUnavailableFallback: boolean;
+ localNoAuthMode: boolean;
+ environment?: string;
+}) {
+ return explicitDemoMode || (environment !== "production" && (authUnavailableFallback || localNoAuthMode));
+}
+
export function publicUploadsEnabled() {
return process.env.NEXT_PUBLIC_PUBLIC_UPLOADS_ENABLED === "true";
}
diff --git a/tests/favourites-demo-boundary.test.ts b/tests/favourites-demo-boundary.test.ts
index a77acd413..0d2761950 100644
--- a/tests/favourites-demo-boundary.test.ts
+++ b/tests/favourites-demo-boundary.test.ts
@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
+import { resolveClientDemoMode } from "@/lib/client-env";
const routeSource = readFileSync(new URL("../src/app/favourites/page.tsx", import.meta.url), "utf8");
const librarySource = readFileSync(
@@ -29,13 +30,40 @@ describe("favourites demo-data boundary", () => {
expect(dashboardSource).toContain("demoMode={clientDemoMode}");
expect(hubSource).toContain("...(demoMode ? favouriteItems : [])");
expect(hubSource).not.toContain("[...favouriteItems, ...savedRegistryFavourites]");
- expect(hubSource).toContain('title="Additional sort options are coming soon"');
- expect(hubSource).toContain('title="Adding favourites from this screen is coming soon"');
- expect(hubSource).toContain('title="Creating favourite sets is coming soon"');
+ expect(hubSource).toContain('aria-describedby="favourites-sort-unavailable"');
+ expect(hubSource).toContain('aria-describedby="favourites-add-unavailable"');
+ expect(hubSource).toContain('aria-describedby="favourites-new-set-unavailable"');
expect(hubSource).toContain("Browse sets");
expect(dashboardSource).not.toContain("Favourite creation is ready to connect.");
expect(globalShellSource).toContain("demoMode={clientDemoMode}");
expect(universalSearchSource).toContain("...(demoMode ? favouriteItems : [])");
expect(universalSearchSource).not.toContain("[...favouriteItems, ...savedRegistryFavourites]");
});
+
+ it("fails closed in production while preserving explicit and non-production demo modes", () => {
+ expect(
+ resolveClientDemoMode({
+ explicitDemoMode: false,
+ authUnavailableFallback: true,
+ localNoAuthMode: false,
+ environment: "production",
+ }),
+ ).toBe(false);
+ expect(
+ resolveClientDemoMode({
+ explicitDemoMode: true,
+ authUnavailableFallback: true,
+ localNoAuthMode: false,
+ environment: "production",
+ }),
+ ).toBe(true);
+ expect(
+ resolveClientDemoMode({
+ explicitDemoMode: false,
+ authUnavailableFallback: true,
+ localNoAuthMode: false,
+ environment: "development",
+ }),
+ ).toBe(true);
+ });
});
diff --git a/tests/favourites-hub-unavailable-controls.dom.test.tsx b/tests/favourites-hub-unavailable-controls.dom.test.tsx
new file mode 100644
index 000000000..0bb48ecf4
--- /dev/null
+++ b/tests/favourites-hub-unavailable-controls.dom.test.tsx
@@ -0,0 +1,33 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { FavouritesHub } from "@/components/clinical-dashboard/favourites-hub";
+
+vi.mock("@/components/clinical-dashboard/use-saved-registry-favourites", () => ({
+ useSavedRegistryFavourites: () => [],
+}));
+
+describe("FavouritesHub unavailable controls", () => {
+ it("keeps unavailable actions focusable and exposes their reasons", () => {
+ render(
undefined} demoMode={false} />);
+
+ const recent = screen.getByRole("button", { name: "Recent" });
+ const add = screen.getByRole("button", { name: /Add favourite/ });
+ const newSet = screen.getByRole("button", { name: "New set" });
+
+ expect(recent).toBeEnabled();
+ expect(recent).toHaveAttribute("aria-disabled", "true");
+ expect(recent).toHaveAccessibleDescription("Additional sort options are coming soon.");
+ expect(add).toBeEnabled();
+ expect(add).toHaveAttribute("aria-disabled", "true");
+ expect(add).toHaveAccessibleDescription("Adding favourites from this screen is coming soon.");
+ expect(newSet).toBeEnabled();
+ expect(newSet).toHaveAttribute("aria-disabled", "true");
+ expect(newSet).toHaveAccessibleDescription("Creating favourite sets is coming soon.");
+
+ for (const control of [recent, add, newSet]) {
+ control.focus();
+ expect(document.activeElement).toBe(control);
+ }
+ });
+});
diff --git a/tests/ui-route-coverage.spec.ts b/tests/ui-route-coverage.spec.ts
index 74b0276a6..e90cb6120 100644
--- a/tests/ui-route-coverage.spec.ts
+++ b/tests/ui-route-coverage.spec.ts
@@ -370,7 +370,11 @@ test.describe("previously uncovered production routes", () => {
await skipLink.focus();
await expect(skipLink).toBeVisible();
await skipLink.press("Enter");
- await expect(currentPage.locator("#main-content")).toBeFocused();
+ const mainContent = currentPage.locator("#main-content");
+ await expect(mainContent).toBeFocused();
+ await expect
+ .poll(() => mainContent.evaluate((element) => window.getComputedStyle(element).outlineStyle))
+ .not.toBe("none");
},
);
});
From baeced6f238379411d9a78149b6d4e5f35470f8a Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Sat, 18 Jul 2026 22:02:05 +0800
Subject: [PATCH 06/21] fix: restrict favourite selection to wide layouts
---
.../favourites-command-library-page.tsx | 2 +-
tests/favourites-demo-boundary.test.ts | 16 +++++++++
tests/ui-smoke.spec.ts | 33 +++++++++++++++++--
3 files changed, 47 insertions(+), 4 deletions(-)
diff --git a/src/components/clinical-dashboard/favourites-command-library-page.tsx b/src/components/clinical-dashboard/favourites-command-library-page.tsx
index b43e90e4c..dcec32318 100644
--- a/src/components/clinical-dashboard/favourites-command-library-page.tsx
+++ b/src/components/clinical-dashboard/favourites-command-library-page.tsx
@@ -698,7 +698,7 @@ function FavouritesTable({
className={cn(
"relative h-14 transition hover:bg-[color:var(--surface-subtle)]",
selected &&
- "bg-[color:var(--clinical-accent-soft)]/45 shadow-[inset_3px_0_0_var(--clinical-accent)]",
+ "xl:bg-[color:var(--clinical-accent-soft)]/45 xl:shadow-[inset_3px_0_0_var(--clinical-accent)]",
)}
>
diff --git a/tests/favourites-demo-boundary.test.ts b/tests/favourites-demo-boundary.test.ts
index 0d2761950..f87d049ad 100644
--- a/tests/favourites-demo-boundary.test.ts
+++ b/tests/favourites-demo-boundary.test.ts
@@ -21,6 +21,10 @@ const universalSearchSource = readFileSync(
new URL("../src/components/clinical-dashboard/universal-search-command-surface.tsx", import.meta.url),
"utf8",
);
+const mobileCardSource = librarySource.slice(
+ librarySource.indexOf("function FavouriteMobileCard"),
+ librarySource.indexOf("function FavouritesTable"),
+);
describe("favourites demo-data boundary", () => {
it("passes trusted server demo state and never merges prototype favourites into live mode unconditionally", () => {
@@ -66,4 +70,16 @@ describe("favourites demo-data boundary", () => {
}),
).toBe(true);
});
+
+ it("limits item selection to xl tables and keeps mobile cards action-only", () => {
+ expect(mobileCardSource).toContain("function FavouriteMobileCard({ item }: { item: FavouriteItem })");
+ expect(mobileCardSource).not.toContain("aria-pressed");
+ expect(mobileCardSource).not.toContain("onSelect");
+ expect(mobileCardSource).toContain(" ");
+ expect(librarySource).toContain('"hidden min-w-0 max-w-full items-center gap-2.5 rounded-md text-left xl:flex"');
+ expect(librarySource).toContain('"block min-w-0 max-w-full rounded-md text-left xl:hidden"');
+ expect(librarySource).toContain(
+ '"xl:bg-[color:var(--clinical-accent-soft)]/45 xl:shadow-[inset_3px_0_0_var(--clinical-accent)]"',
+ );
+ });
});
diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts
index e84f9c199..82a78d35b 100644
--- a/tests/ui-smoke.spec.ts
+++ b/tests/ui-smoke.spec.ts
@@ -2684,15 +2684,42 @@ test.describe("Clinical KB UI smoke coverage", () => {
await expect(moreActions).toBeFocused();
});
- test("favourites mobile cards keep their links and menus as separate native controls", async ({ page }) => {
+ test("favourites disable item selection below xl while keeping navigation and actions", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await mockDemoApi(page);
await gotoApp(page, "/favourites");
const hub = page.getByTestId("favourites-hub");
await expect(hub.locator('article[role="button"]')).toHaveCount(0);
- await expect(hub.getByRole("link", { name: "Open Acamprosate renal screen" })).toBeVisible();
- await expect(hub.getByRole("button", { name: "More actions for Acamprosate renal screen" })).toBeVisible();
+ const card = hub.locator("article").filter({ hasText: "Acamprosate renal screen" });
+ const openItem = card.getByRole("link", { name: "Open Acamprosate renal screen" });
+ const moreActions = card.getByRole("button", { name: "More actions for Acamprosate renal screen" });
+
+ await expect(card).toBeVisible();
+ await expect(card.locator("button[aria-pressed]")).toHaveCount(0);
+ await expectMinTouchTarget(openItem);
+ await expectMinTouchTarget(moreActions);
+ await expectNoPageHorizontalOverflow(page);
+
+ await page.setViewportSize({ width: 1180, height: 820 });
+ const row = page.getByTestId("favourite-row-acamprosate-renal-screen");
+ await expect(row).toBeVisible();
+ await expect(row.locator("button[aria-pressed]")).toBeHidden();
+ await expect(row.locator("td").first().getByRole("link")).toBeVisible();
+ await expect(row.getByRole("link", { name: "Open Acamprosate renal screen" })).toBeVisible();
+ await expect(row.getByRole("button", { name: "More actions for Acamprosate renal screen" })).toBeVisible();
+
+ await page.setViewportSize({ width: 1536, height: 900 });
+ const selectItem = row.locator("button[aria-pressed]");
+ await expect(selectItem).toBeVisible();
+ await selectItem.click();
+ await expect(page.getByTestId("favourites-item-workspace")).toBeVisible();
+
+ await page.setViewportSize({ width: 1180, height: 820 });
+ await expect(page.getByTestId("favourites-item-workspace")).toBeHidden();
+ await expect(selectItem).toBeHidden();
+ await expect(row).not.toHaveClass(/(^|\s)bg-\[/);
+ await expectNoPageHorizontalOverflow(page);
});
test("app mode menu supports keyboard navigation without removed prototype modes", async ({ page }) => {
From 4b6b83bce33d2f5f4432367048b25f27ac63f2b1 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 18 Jul 2026 15:55:36 +0000
Subject: [PATCH 07/21] fix: keep therapy count loading-safe and local no-auth
uploads writable
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Gate Therapy Compass home on an empty loading catalogue so it never
advertises “0 records”, and resolve upload read-only without treating
local no-auth as demo lockout.
Co-authored-by: BigSimmo
---
src/components/ClinicalDashboard.tsx | 8 ++++-
.../therapy-compass/data/use-therapy-data.ts | 5 +++
.../therapy-compass/screens/home-screen.tsx | 13 +++++--
.../audit-navigation-auth-regressions.test.ts | 9 +++++
tests/favourites-demo-boundary.test.ts | 23 ++++++++++++
...therapy-compass-data-recovery.dom.test.tsx | 36 +++++++++++++++++++
6 files changed, 91 insertions(+), 3 deletions(-)
diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx
index 643532a72..2919b29d5 100644
--- a/src/components/ClinicalDashboard.tsx
+++ b/src/components/ClinicalDashboard.tsx
@@ -838,7 +838,13 @@ export function ClinicalDashboard({
setAnswerThreadBootstrapped(true);
});
}, [answerThreadOwnerId, authStatus]);
- const uploadReadOnlyMode = clientDemoMode;
+ // Local no-auth still has private upload APIs (`canUsePrivateApis`); do not lock the
+ // upload drawer just because `resolveClientDemoMode` treats no-auth as demo for favourites.
+ const uploadReadOnlyMode = resolveClientDemoMode({
+ explicitDemoMode,
+ authUnavailableFallback: browserAuthUnavailableDemoFallback,
+ localNoAuthMode: false,
+ });
const localDevCanAttemptPrivateApis = process.env.NODE_ENV !== "production" && hasReadyPublicSearchSetup(setupChecks);
const canUsePublicSearchApis = localProjectReady && hasReadyPublicSearchSetup(setupChecks);
const canUseDegradedLocalSearchApis =
diff --git a/src/components/therapy-compass/data/use-therapy-data.ts b/src/components/therapy-compass/data/use-therapy-data.ts
index f04d4a702..1b83416d4 100644
--- a/src/components/therapy-compass/data/use-therapy-data.ts
+++ b/src/components/therapy-compass/data/use-therapy-data.ts
@@ -13,6 +13,11 @@ const BASE = "/therapy-compass-data";
// shared across every screen.
let cache: Promise | null = null;
+/** Test helper: drop the session-scoped dataset cache between cases. */
+export function clearTherapyDataCache() {
+ cache = null;
+}
+
async function fetchJson(path: string): Promise {
const res = await fetch(path);
if (!res.ok) throw new Error(`Failed to load ${path}: ${res.status}`);
diff --git a/src/components/therapy-compass/screens/home-screen.tsx b/src/components/therapy-compass/screens/home-screen.tsx
index 27455d74c..a7f1b0655 100644
--- a/src/components/therapy-compass/screens/home-screen.tsx
+++ b/src/components/therapy-compass/screens/home-screen.tsx
@@ -7,6 +7,7 @@ import { commandControl, linkButton } from "../controls";
import type { Therapy } from "../data/types";
import { ChevronRightIcon, CompassIcon, FileTextIcon, PathwayIcon, ScaleIcon, SearchIcon, SparkleIcon } from "../icons";
import { s } from "../style-utils";
+import { LoadingState } from "../ui";
const SUGGESTIONS = [
"Anxiety in outpatient care",
@@ -28,10 +29,19 @@ export function HomeScreen() {
const b = useTcBindings();
const [query, setLocalQuery] = useState("");
+ // First paint / slow fetch: therapies is [] while loading — avoid advertising “0 records”.
+ if (b.loading && b.therapies.length === 0) {
+ return ;
+ }
+
const bySlug = new Map(b.therapies.map((t) => [t.slug, t]));
const featured: Therapy[] = FEATURED_SLUGS.map((sl) => bySlug.get(sl)).filter((t): t is Therapy => Boolean(t));
const featuredList = (featured.length ? featured : b.therapies).slice(0, 6);
const pathways = b.pathways.slice(0, 3);
+ const therapyCountCopy =
+ b.therapies.length === 0
+ ? "Search source-grounded therapy records by problem, symptom, skill or population — or jump into a clinical pathway."
+ : `Search ${b.therapies.length} source-grounded therapy ${b.therapies.length === 1 ? "record" : "records"} by problem, symptom, skill or population — or jump into a clinical pathway.`;
const submit = () => b.submitQuery(query);
@@ -51,8 +61,7 @@ export function HomeScreen() {
What therapy are you looking for?
- Search {b.therapies.length} source-grounded therapy {b.therapies.length === 1 ? "record" : "records"} by
- problem, symptom, skill or population — or jump into a clinical pathway.
+ {therapyCountCopy}
diff --git a/tests/audit-navigation-auth-regressions.test.ts b/tests/audit-navigation-auth-regressions.test.ts
index 73973d114..0459ee460 100644
--- a/tests/audit-navigation-auth-regressions.test.ts
+++ b/tests/audit-navigation-auth-regressions.test.ts
@@ -90,6 +90,15 @@ describe("audit navigation and auth regressions", () => {
});
it("gates private polling and mutations on local readiness plus authenticated status", () => {
+ const uploadReadOnlyContract = sourceSegment(
+ clinicalDashboardSource,
+ "const uploadReadOnlyMode =",
+ "const canUsePrivateApis =",
+ );
+ expect(uploadReadOnlyContract).toContain("const uploadReadOnlyMode = resolveClientDemoMode({");
+ expect(uploadReadOnlyContract).toContain("localNoAuthMode: false");
+ expect(uploadReadOnlyContract).not.toMatch(/const uploadReadOnlyMode = clientDemoMode\b/);
+
const privateCapabilityContract = sourceSegment(
clinicalDashboardSource,
"const canUsePrivateApis =",
diff --git a/tests/favourites-demo-boundary.test.ts b/tests/favourites-demo-boundary.test.ts
index 0d2761950..85157fa8f 100644
--- a/tests/favourites-demo-boundary.test.ts
+++ b/tests/favourites-demo-boundary.test.ts
@@ -66,4 +66,27 @@ describe("favourites demo-data boundary", () => {
}),
).toBe(true);
});
+
+ it("keeps upload read-only independent of local no-auth demo treatment", () => {
+ // Local no-auth is demo for favourites/recent-query owners, but uploads must stay writable.
+ expect(
+ resolveClientDemoMode({
+ explicitDemoMode: false,
+ authUnavailableFallback: false,
+ localNoAuthMode: true,
+ environment: "development",
+ }),
+ ).toBe(true);
+ expect(
+ resolveClientDemoMode({
+ explicitDemoMode: false,
+ authUnavailableFallback: false,
+ localNoAuthMode: false,
+ environment: "development",
+ }),
+ ).toBe(false);
+ expect(dashboardSource).toContain("localNoAuthMode: false");
+ expect(dashboardSource).toMatch(/const uploadReadOnlyMode = resolveClientDemoMode\(\{/);
+ expect(dashboardSource).not.toMatch(/const uploadReadOnlyMode = clientDemoMode\b/);
+ });
});
diff --git a/tests/therapy-compass-data-recovery.dom.test.tsx b/tests/therapy-compass-data-recovery.dom.test.tsx
index d69965cff..32c14907b 100644
--- a/tests/therapy-compass-data-recovery.dom.test.tsx
+++ b/tests/therapy-compass-data-recovery.dom.test.tsx
@@ -2,6 +2,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { TherapyCompassWorkspace } from "@/components/therapy-compass";
+import { clearTherapyDataCache } from "@/components/therapy-compass/data/use-therapy-data";
import { HomeScreen } from "@/components/therapy-compass/screens/home-screen";
vi.mock("next/navigation", () => ({
@@ -28,10 +29,45 @@ function response(body: unknown, ok = true, status = 200) {
}
afterEach(() => {
+ clearTherapyDataCache();
vi.unstubAllGlobals();
});
describe("Therapy Compass required data recovery", () => {
+ it("does not advertise a zero therapy count while the catalogue is still loading", async () => {
+ let release!: (value: unknown) => void;
+ const therapiesGate = new Promise((resolve) => {
+ release = resolve;
+ });
+ const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
+ const path = String(input);
+ if (path.endsWith("/therapies.json")) {
+ await therapiesGate;
+ return response([therapy]);
+ }
+ if (path.endsWith("/pathways.json")) return response([]);
+ if (path.endsWith("/reference.json")) return response({});
+ throw new Error(`Unexpected fetch: ${path}`);
+ });
+ vi.stubGlobal("fetch", fetchMock);
+
+ render(
+
+
+ ,
+ );
+
+ expect(await screen.findByRole("status")).toHaveTextContent("Loading therapy library…");
+ expect(screen.queryByText(/Search 0 source-grounded therapy/)).not.toBeInTheDocument();
+ expect(screen.queryByRole("heading", { name: "What therapy are you looking for?" })).not.toBeInTheDocument();
+
+ release(undefined);
+
+ expect(await screen.findByRole("heading", { name: "What therapy are you looking for?" })).toBeInTheDocument();
+ expect(screen.getByText(/Search 1 source-grounded therapy record by/)).toBeInTheDocument();
+ expect(screen.queryByText(/Search 0 source-grounded therapy/)).not.toBeInTheDocument();
+ });
+
it("shows an honest load error, retries all required files, and recovers", async () => {
let failTherapies = true;
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
From 5a52cbb4cd83b49807c4422bc425f278fc253b83 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 18 Jul 2026 15:59:59 +0000
Subject: [PATCH 08/21] style: prettier therapy home loading-safe count copy
Co-authored-by: BigSimmo
---
src/components/therapy-compass/screens/home-screen.tsx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/components/therapy-compass/screens/home-screen.tsx b/src/components/therapy-compass/screens/home-screen.tsx
index a7f1b0655..8bc001ff6 100644
--- a/src/components/therapy-compass/screens/home-screen.tsx
+++ b/src/components/therapy-compass/screens/home-screen.tsx
@@ -60,9 +60,7 @@ export function HomeScreen() {
>
What therapy are you looking for?
-
- {therapyCountCopy}
-
+ {therapyCountCopy}
Date: Sun, 19 Jul 2026 00:03:28 +0800
Subject: [PATCH 09/21] fix: resolve final design audit regressions
---
docs/branch-review-ledger.md | 2 +-
docs/design-audit-2026-07-17.md | 8 +-
src/components/ClinicalDashboard.tsx | 73 ++++++++++++++++---
src/components/applications-launcher-page.tsx | 11 +--
.../therapy-compass/screens/home-screen.tsx | 3 +
.../audit-navigation-auth-regressions.test.ts | 8 +-
...therapy-compass-data-recovery.dom.test.tsx | 4 +
tests/ui-smoke.spec.ts | 1 +
tests/ui-tools.spec.ts | 2 +
9 files changed, 85 insertions(+), 27 deletions(-)
diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md
index fea8c3898..7c56dab8d 100644
--- a/docs/branch-review-ledger.md
+++ b/docs/branch-review-ledger.md
@@ -608,4 +608,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-17 | codex/design-audit-20260716 | 47b58ddf95826231a6057b400e7f601c403a129d + reviewed working diff | exhaustive design, accessibility, UX, interaction, route, responsive, HTML/CSS/JS/TSX, and test-coverage audit with remediation | No P0. Fixed two P1 trust/responsive defects and the confirmed P2/P3 set: false Favourites provenance/demo leakage/dead controls, Therapy phone reflow/load recovery/contrast, semantic tabs/search/filters, error focus/theme, deterministic Forms back navigation, honest unavailable actions, application dialog semantics, duplicate differential keys, skip-link target, Playwright project isolation, and route coverage. External visual-target fidelity remains unavailable. | Static 261-file/~67,944-line inventory; independent combined-diff review; focused Vitest 21/21; jsdom 24/24; architecture 6/6; accessibility browser 5/5; focused route/browser recovery; lint; TypeScript; runtime/workflow/sitemap/brand/type/icon checks; production build with 1,043 pages and client-secret scan; `git diff --check`. Canonical aggregate Vitest was Windows-runner inconclusive and full UI was not repeated after focused remediation. No provider/API checks. |
| 2026-07-18 | codex/main-merge-51278-final-20260718-late | 45fa3c6c7, 14a0a898c | merge integration of 51278a70d onto fresh origin/main | Replayed the requested historical design-audit commit onto fresh `origin/main`. Kept current-main versions for five conflicts, including the regenerated drift manifest and current schema assertions. Fixed the duplicate sitemap-generator declaration, corrected redirect-section coverage, and restored the current `Clinical Guide` UI contract. | `git diff --check` and `npm run sitemap:check` passed. `npm run test` was attempted twice but blocked by the repository-wide heavy-command lock held by a separate Playwright worktree; no provider-backed checks ran. |
| 2026-07-18 | origin/main framework and dependency modernization snapshot | 4057677c8b92a5e1d997ec44958764fa91f5d424 | parallel build/infra, backend, and frontend modernization audit | Changes requested. No P0. Confirmed two P1 defects: Supabase SSR 0.12 auth-cookie responses discard mandatory anti-cache headers, and reindex bypasses the server-only-aware TSX runner after mutation-capable setup. Five P2 blockers cover the Webpack-to-Turbopack production cutover, incomplete Railway image-build watch ownership, missing clean `next typegen`, Node 26 types over a Node 24 runtime, and App Router retry actions that reset without re-fetching. P3 removal-readiness debt remains in Zod and Next Image APIs. Manual rewrite zones are auth response ownership, bundler/CSP/artifact consumers, JSZip resource limits, OpenAI request typing, and React Compiler adoption in the stateful dashboard/viewer roots. | Parallel read-only source/config/test audit against the exact snapshot; Node 24.18.0/npm 11.17.0; exact-version Next 16.2.10 bundled upgrade, Turbopack, error-boundary, and codemod guidance; TypeScript 6 backend no-emit analysis and Node import probes via a separately installed exact-version local dependency tree; `git diff --check`. No install, registry outdated/audit, full repo lint/typecheck/test/build/browser/Docker, Supabase/OpenAI, deployment, or hosted CI run; this worktree had no `node_modules`. |
-| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | c5e7b96c3 + reviewed follow-up diff | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and disposition of all four CodeRabbit comments. The follow-up made client demo state fail closed in production, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, and removed the sole zero-warning CI lint failure. External design-target fidelity remains unverified without an approved target. | Local PR plan: formatting, full ESLint, TypeScript, 309 files / 2,797 Vitest tests, webpack production build with 1,674 pages and client-secret scan, and 36 offline RAG fixtures across 21 suites. Initial hosted Production UI passed on `8071a6704`; focused review-fix Vitest 3/3 and zero-warning ESLint passed; formatting and diff checks passed. A post-fix local typecheck was deferred because another registered worktree held the heavyweight lock; exact-head hosted static/type/UI checks remain required after push. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. |
+| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | c5e7b96c3 + baeced6f2 + reviewed follow-up diff | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. Initial exact-head hosted checks, including Production UI, passed before these final follow-ups; exact-head hosted static/type/UI checks remain required after the final push. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. |
diff --git a/docs/design-audit-2026-07-17.md b/docs/design-audit-2026-07-17.md
index 2eb0cfde8..a8654592f 100644
--- a/docs/design-audit-2026-07-17.md
+++ b/docs/design-audit-2026-07-17.md
@@ -29,10 +29,10 @@ This pass deliberately built on the 2026-07-15 live design audit instead of repe
| DQA-08 | P2 | Evidence records with no URL were still keyboard-focusable `href="#"` links announced as “Open source”. | Missing-source rows now render as non-links with unavailable semantics; only valid destinations render a Link. | `visual-evidence.tsx`, `visual-evidence-tabs.dom.test.tsx` |
| DQA-09 | P2 | Therapy cards presented an enabled Favourite button with no handler or state change. | The unavailable feature is now disabled and labelled as coming soon, preventing false save confirmation. | `therapy-card.tsx` |
| DQA-10 | P2 | Route/global error fallbacks did not reliably move focus or announce a route failure; the global fallback was hard-coded light-only. | Added programmatic heading focus and alert announcement, and made the self-contained global fallback system-colour, dark-mode, and forced-colour aware. | `route-error-boundary.tsx`, `global-error.tsx`, route error tests |
-| DQA-11 | P2 | Clinical Notes, Visual Evidence, and upload/index tab interfaces lacked complete roving keyboard behavior and reciprocal tab/panel relationships. | Added stable unique IDs, `aria-controls`/`aria-labelledby`, roving `tabIndex`, and Left/Right/Home/End auto-activation. | `ClinicalDashboard.tsx`, `answer-result-surface.tsx`, `visual-evidence.tsx`, UI/DOM tests |
+| DQA-11 | P2 | Clinical Notes, Visual Evidence, and upload/index tab interfaces lacked complete roving keyboard behavior and reciprocal tab/panel relationships. | Added stable unique IDs, `aria-controls`/`aria-labelledby`, roving `tabIndex`, and Left/Right/Home/End auto-activation. Upload/indexing uses tab panels only with its visible mobile tablist and named regions when desktop renders every section. | `ClinicalDashboard.tsx`, `answer-result-surface.tsx`, `visual-evidence.tsx`, UI/DOM tests |
| DQA-12 | P2 | Universal search visually highlighted an item without exposing it through `aria-activedescendant`. | The combobox now points to the active option and tests verify the relationship through keyboard navigation. | `master-search-header.tsx`, `universal-search-command-surface.tsx`, `ui-universal-search.spec.ts` |
-| DQA-13 | P2 | Differential filters and several selector groups behaved as toggles but lacked grouped/pressed semantics. | Added labelled groups and stateful `aria-pressed` semantics, with browser accessibility assertions. | `differentials-home.tsx`, `evidence-panels.tsx`, `ui-accessibility.spec.ts` |
-| DQA-14 | P2 | Therapy Compass silently swallowed data-load failure and still implied a fabricated `200+` catalogue count. | Added an explicit failure state and Retry action, removed fabricated totals, and retained real successful/empty states. The final integration tests the routed workspace that replaced the legacy single-page wrapper. | `use-therapy-data.ts`, `workspace.tsx`, `therapy-compass-data-recovery.dom.test.tsx` |
+| DQA-13 | P2 | Differential filters and several selector groups behaved as toggles but lacked grouped/pressed semantics; the Tools result set retained orphaned tab-panel semantics after its controls became pressed filters. | Added labelled groups and stateful `aria-pressed` semantics, and exposed Tools results as a labelled group, with browser accessibility assertions. | `differentials-home.tsx`, `evidence-panels.tsx`, `applications-launcher-page.tsx`, `ui-accessibility.spec.ts`, `ui-tools.spec.ts` |
+| DQA-14 | P2 | Therapy Compass silently swallowed data-load failure, rendered an empty `0`-record catalogue while loading, and still implied a fabricated `200+` catalogue count. | Added distinct loading and failure states, a Retry action, removed fabricated totals, and retained real successful/empty states. The final integration tests the routed workspace that replaced the legacy single-page wrapper. | `use-therapy-data.ts`, `workspace.tsx`, `home-screen.tsx`, `therapy-compass-data-recovery.dom.test.tsx` |
| DQA-15 | P2 | Direct-entry Forms “Back to forms” used browser history and could leave the application. | The control now deterministically navigates to `/forms?focus=1`; a DOM regression test verifies the destination. | `form-detail-page.tsx`, `forms-back-navigation.dom.test.tsx` |
| DQA-16 | P2 | Firefox/WebKit production projects unintentionally collected advisory `@mockup` tests; two browser helpers also retried hydration inconsistently. | Applied production matchers and `grepInvert` to all required projects and introduced one shared app-mode hydration retry helper. | `playwright.config.ts`, `playwright-app-mode.ts`, `ui-stress.spec.ts`, `ui-tools.spec.ts` |
| DQA-17 | P2 | Application/tool cards looked like direct links but prevented navigation to open an internal detail state, obscuring the real action. | Cards are now honest dialog buttons with `aria-haspopup="dialog"`; the dialog contains the actual destination link. | `applications-launcher-page.tsx`, `ui-smoke.spec.ts` |
@@ -43,7 +43,7 @@ This pass deliberately built on the 2026-07-15 live design audit instead of repe
## Route and component interaction proof
-The new provider-free route matrix covers Therapy Compass, DSM home/compare/differentials, Specifier compare/map, differential diagnosis stream/detail, colour reference, Applications, Medications and differential-presentation redirects, and both document-source aliases at desktop and 390 px. It asserts visible primary content, no document-level horizontal overflow, a representative action, blocked external/API leakage, and route-specific behavior.
+The new provider-free route matrix covers Therapy Compass, DSM home/compare/differentials, Specifier compare/map, differential diagnosis stream/detail, colour reference, Applications, Medications and differential-presentation redirects, and the invalid document-source alias at desktop and 390 px. It asserts visible primary content, no document-level horizontal overflow, a representative action, blocked external/API leakage, and route-specific behavior. The valid `/documents/source` case reached the expected redirect request but did not complete its final browser assertion, as recorded below.
Additional focused browser proof covered:
diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx
index 643532a72..3e50847c1 100644
--- a/src/components/ClinicalDashboard.tsx
+++ b/src/components/ClinicalDashboard.tsx
@@ -32,6 +32,7 @@ import {
useReducer,
useRef,
useState,
+ useSyncExternalStore,
} from "react";
import { type DocumentDeleteResult } from "@/components/DocumentManagementActions";
import { extractSafetyFindings } from "@/lib/clinical-safety";
@@ -123,6 +124,23 @@ const FavouritesHub = dynamic(
() => import("@/components/clinical-dashboard/favourites-hub").then((m) => m.FavouritesHub),
{ ssr: false },
);
+
+const uploadDesktopMediaQuery = "(min-width: 1024px)";
+
+function subscribeToUploadDesktopLayout(callback: () => void) {
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => {};
+ const media = window.matchMedia(uploadDesktopMediaQuery);
+ media.addEventListener("change", callback);
+ return () => media.removeEventListener("change", callback);
+}
+
+function getUploadDesktopLayoutSnapshot() {
+ return (
+ typeof window !== "undefined" &&
+ typeof window.matchMedia === "function" &&
+ window.matchMedia(uploadDesktopMediaQuery).matches
+ );
+}
const MedicationPrescribingWorkspace = dynamic(
() =>
import("@/components/clinical-dashboard/medication-prescribing-workspace").then(
@@ -661,6 +679,11 @@ export function ClinicalDashboard({
const [documentsDrawerMode, setDocumentsDrawerMode] = useState
("library");
const [uploadDrawerOpen, setUploadDrawerOpen] = useState(false);
const [uploadMobileTab, setUploadMobileTab] = useState("upload");
+ const uploadUsesDesktopRegions = useSyncExternalStore(
+ subscribeToUploadDesktopLayout,
+ getUploadDesktopLayoutSnapshot,
+ () => false,
+ );
const uploadTabRefs = useRef(new Map());
const [documentDrawerStatusFilter, setDocumentDrawerStatusFilter] = useState("indexed");
const [indexingMonitorFilter, setIndexingMonitorFilter] = useState("all");
@@ -4063,14 +4086,19 @@ export function ClinicalDashboard({
-
+
Developer setup status
@@ -4078,14 +4106,21 @@ export function ClinicalDashboard({
-
+
Clinical upload
-
+
Indexing progress
-
+
Ingestion quality console
filter.id === activeFilter)?.label ??
mobileFilters.find((filter) => filter.id === activeFilter)?.label;
@@ -796,12 +796,7 @@ export function ApplicationsLauncherWorkspace({
-
+
{filteredApps.length === 0 ? (
{copy.emptyTitle}
diff --git a/src/components/therapy-compass/screens/home-screen.tsx b/src/components/therapy-compass/screens/home-screen.tsx
index 27455d74c..b2b917968 100644
--- a/src/components/therapy-compass/screens/home-screen.tsx
+++ b/src/components/therapy-compass/screens/home-screen.tsx
@@ -7,6 +7,7 @@ import { commandControl, linkButton } from "../controls";
import type { Therapy } from "../data/types";
import { ChevronRightIcon, CompassIcon, FileTextIcon, PathwayIcon, ScaleIcon, SearchIcon, SparkleIcon } from "../icons";
import { s } from "../style-utils";
+import { LoadingState } from "../ui";
const SUGGESTIONS = [
"Anxiety in outpatient care",
@@ -28,6 +29,8 @@ export function HomeScreen() {
const b = useTcBindings();
const [query, setLocalQuery] = useState("");
+ if (b.loading) return
;
+
const bySlug = new Map(b.therapies.map((t) => [t.slug, t]));
const featured: Therapy[] = FEATURED_SLUGS.map((sl) => bySlug.get(sl)).filter((t): t is Therapy => Boolean(t));
const featuredList = (featured.length ? featured : b.therapies).slice(0, 6);
diff --git a/tests/audit-navigation-auth-regressions.test.ts b/tests/audit-navigation-auth-regressions.test.ts
index 73973d114..7041e4913 100644
--- a/tests/audit-navigation-auth-regressions.test.ts
+++ b/tests/audit-navigation-auth-regressions.test.ts
@@ -130,10 +130,14 @@ describe("audit navigation and auth regressions", () => {
expect(clinicalDashboardSource).toContain("aria-selected={active}");
expect(clinicalDashboardSource).toContain("aria-controls={tab.panelId}");
expect(clinicalDashboardSource).toContain("tabIndex={active ? 0 : -1}");
- expect(clinicalDashboardSource).toContain('role="tabpanel"');
+ expect(clinicalDashboardSource).toContain('role={uploadUsesDesktopRegions ? "region" : "tabpanel"}');
for (const tab of ["setup", "upload", "jobs", "quality"]) {
- expect(clinicalDashboardSource).toContain(`aria-labelledby="dashboard-upload-tab-${tab}"`);
+ expect(clinicalDashboardSource).toContain(`"dashboard-upload-tab-${tab}"`);
}
+ for (const section of ["setup", "upload", "indexing", "quality"]) {
+ expect(clinicalDashboardSource).toContain(`id="dashboard-${section}-section-heading"`);
+ }
+ expect(clinicalDashboardSource).toContain("subscribeToUploadDesktopLayout");
expect(clinicalDashboardSource).toContain('event.key === "ArrowRight"');
expect(clinicalDashboardSource).toContain('event.key === "ArrowLeft"');
expect(clinicalDashboardSource).toContain('event.key === "Home"');
diff --git a/tests/therapy-compass-data-recovery.dom.test.tsx b/tests/therapy-compass-data-recovery.dom.test.tsx
index d69965cff..d964d39a6 100644
--- a/tests/therapy-compass-data-recovery.dom.test.tsx
+++ b/tests/therapy-compass-data-recovery.dom.test.tsx
@@ -51,6 +51,10 @@ describe("Therapy Compass required data recovery", () => {
,
);
+ expect(screen.getByRole("status")).toHaveTextContent("Loading therapy catalogue");
+ expect(screen.queryByText(/Search 0 source-grounded therapy records/)).not.toBeInTheDocument();
+ expect(screen.queryByRole("heading", { name: "Frequently used therapies" })).not.toBeInTheDocument();
+
expect(await screen.findByRole("alert")).toHaveTextContent("Therapy Compass could not load");
expect(screen.queryByRole("heading", { name: "What therapy are you looking for?" })).not.toBeInTheDocument();
diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts
index 82a78d35b..d06284c2f 100644
--- a/tests/ui-smoke.spec.ts
+++ b/tests/ui-smoke.spec.ts
@@ -2728,6 +2728,7 @@ test.describe("Clinical KB UI smoke coverage", () => {
await gotoApp(page, "/?mode=answer");
const appModeButton = page.getByRole("button", { name: "Mode Answer" });
+ await waitForReactEventHandler(appModeButton, "onClick");
await appModeButton.click();
const appModeMenu = page.getByRole("menu", { name: "Choose app mode" });
await expect(appModeMenu).toBeVisible();
diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts
index 27d4df2f0..994a414b6 100644
--- a/tests/ui-tools.spec.ts
+++ b/tests/ui-tools.spec.ts
@@ -364,6 +364,8 @@ test.describe("Clinical KB tools launcher", () => {
await expect(page.getByRole("heading", { level: 1, name: "Tools" })).toBeVisible();
await expect(page.getByRole("region", { name: "Quick tool shortcuts" })).toBeVisible();
await expect(page.getByRole("heading", { name: "All tools" })).toBeVisible();
+ await expect(page.locator("#launcher-results-panel")).toHaveAttribute("role", "group");
+ await expect(page.locator("#launcher-results-panel")).toHaveAttribute("aria-label", "All tools");
if (viewport.name === "mobile") {
await page.getByTestId("application-row-medication-prescribing").click();
const selectedSheet = page.getByRole("dialog", { name: "Medication Prescribing" });
From e5abc6b59572f146fb59d5440178035747ce7474 Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Sun, 19 Jul 2026 00:06:08 +0800
Subject: [PATCH 10/21] docs: record final design audit verification
---
docs/branch-review-ledger.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md
index 248adf498..bbea51f7e 100644
--- a/docs/branch-review-ledger.md
+++ b/docs/branch-review-ledger.md
@@ -610,4 +610,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-17 | codex/design-audit-20260716 | 47b58ddf95826231a6057b400e7f601c403a129d + reviewed working diff | exhaustive design, accessibility, UX, interaction, route, responsive, HTML/CSS/JS/TSX, and test-coverage audit with remediation | No P0. Fixed two P1 trust/responsive defects and the confirmed P2/P3 set: false Favourites provenance/demo leakage/dead controls, Therapy phone reflow/load recovery/contrast, semantic tabs/search/filters, error focus/theme, deterministic Forms back navigation, honest unavailable actions, application dialog semantics, duplicate differential keys, skip-link target, Playwright project isolation, and route coverage. External visual-target fidelity remains unavailable. | Static 261-file/~67,944-line inventory; independent combined-diff review; focused Vitest 21/21; jsdom 24/24; architecture 6/6; accessibility browser 5/5; focused route/browser recovery; lint; TypeScript; runtime/workflow/sitemap/brand/type/icon checks; production build with 1,043 pages and client-secret scan; `git diff --check`. Canonical aggregate Vitest was Windows-runner inconclusive and full UI was not repeated after focused remediation. No provider/API checks. |
| 2026-07-18 | codex/main-merge-51278-final-20260718-late | 45fa3c6c7, 14a0a898c | merge integration of 51278a70d onto fresh origin/main | Replayed the requested historical design-audit commit onto fresh `origin/main`. Kept current-main versions for five conflicts, including the regenerated drift manifest and current schema assertions. Fixed the duplicate sitemap-generator declaration, corrected redirect-section coverage, and restored the current `Clinical Guide` UI contract. | `git diff --check` and `npm run sitemap:check` passed. `npm run test` was attempted twice but blocked by the repository-wide heavy-command lock held by a separate Playwright worktree; no provider-backed checks ran. |
| 2026-07-18 | origin/main framework and dependency modernization snapshot | 4057677c8b92a5e1d997ec44958764fa91f5d424 | parallel build/infra, backend, and frontend modernization audit | Changes requested. No P0. Confirmed two P1 defects: Supabase SSR 0.12 auth-cookie responses discard mandatory anti-cache headers, and reindex bypasses the server-only-aware TSX runner after mutation-capable setup. Five P2 blockers cover the Webpack-to-Turbopack production cutover, incomplete Railway image-build watch ownership, missing clean `next typegen`, Node 26 types over a Node 24 runtime, and App Router retry actions that reset without re-fetching. P3 removal-readiness debt remains in Zod and Next Image APIs. Manual rewrite zones are auth response ownership, bundler/CSP/artifact consumers, JSZip resource limits, OpenAI request typing, and React Compiler adoption in the stateful dashboard/viewer roots. | Parallel read-only source/config/test audit against the exact snapshot; Node 24.18.0/npm 11.17.0; exact-version Next 16.2.10 bundled upgrade, Turbopack, error-boundary, and codemod guidance; TypeScript 6 backend no-emit analysis and Node import probes via a separately installed exact-version local dependency tree; `git diff --check`. No install, registry outdated/audit, full repo lint/typecheck/test/build/browser/Docker, Supabase/OpenAI, deployment, or hosted CI run; this worktree had no `node_modules`. |
-| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | c5e7b96c3 + baeced6f2 + reviewed follow-up diff | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. Initial exact-head hosted checks, including Production UI, passed before these final follow-ups; exact-head hosted static/type/UI checks remain required after the final push. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. |
+| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | c5e7b96c3 + baeced6f2 + reviewed follow-up diff | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. The full local Chromium sweep completed with one failure: the app-mode test clicked before React attached its handler. The existing handler-readiness pattern was applied; two focused rerun attempts were then lock-blocked by another registered worktree, so exact-head hosted Chromium remains the required proof. Initial exact-head hosted checks, including Production UI, passed before these final follow-ups. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. |
From 4911bace36754e1a30cca47aaf7d66d4e7ba8f5f Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Sun, 19 Jul 2026 00:07:25 +0800
Subject: [PATCH 11/21] fix: resolve final design audit findings
---
docs/branch-review-ledger.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md
index bbea51f7e..e9b66b7c6 100644
--- a/docs/branch-review-ledger.md
+++ b/docs/branch-review-ledger.md
@@ -610,4 +610,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-17 | codex/design-audit-20260716 | 47b58ddf95826231a6057b400e7f601c403a129d + reviewed working diff | exhaustive design, accessibility, UX, interaction, route, responsive, HTML/CSS/JS/TSX, and test-coverage audit with remediation | No P0. Fixed two P1 trust/responsive defects and the confirmed P2/P3 set: false Favourites provenance/demo leakage/dead controls, Therapy phone reflow/load recovery/contrast, semantic tabs/search/filters, error focus/theme, deterministic Forms back navigation, honest unavailable actions, application dialog semantics, duplicate differential keys, skip-link target, Playwright project isolation, and route coverage. External visual-target fidelity remains unavailable. | Static 261-file/~67,944-line inventory; independent combined-diff review; focused Vitest 21/21; jsdom 24/24; architecture 6/6; accessibility browser 5/5; focused route/browser recovery; lint; TypeScript; runtime/workflow/sitemap/brand/type/icon checks; production build with 1,043 pages and client-secret scan; `git diff --check`. Canonical aggregate Vitest was Windows-runner inconclusive and full UI was not repeated after focused remediation. No provider/API checks. |
| 2026-07-18 | codex/main-merge-51278-final-20260718-late | 45fa3c6c7, 14a0a898c | merge integration of 51278a70d onto fresh origin/main | Replayed the requested historical design-audit commit onto fresh `origin/main`. Kept current-main versions for five conflicts, including the regenerated drift manifest and current schema assertions. Fixed the duplicate sitemap-generator declaration, corrected redirect-section coverage, and restored the current `Clinical Guide` UI contract. | `git diff --check` and `npm run sitemap:check` passed. `npm run test` was attempted twice but blocked by the repository-wide heavy-command lock held by a separate Playwright worktree; no provider-backed checks ran. |
| 2026-07-18 | origin/main framework and dependency modernization snapshot | 4057677c8b92a5e1d997ec44958764fa91f5d424 | parallel build/infra, backend, and frontend modernization audit | Changes requested. No P0. Confirmed two P1 defects: Supabase SSR 0.12 auth-cookie responses discard mandatory anti-cache headers, and reindex bypasses the server-only-aware TSX runner after mutation-capable setup. Five P2 blockers cover the Webpack-to-Turbopack production cutover, incomplete Railway image-build watch ownership, missing clean `next typegen`, Node 26 types over a Node 24 runtime, and App Router retry actions that reset without re-fetching. P3 removal-readiness debt remains in Zod and Next Image APIs. Manual rewrite zones are auth response ownership, bundler/CSP/artifact consumers, JSZip resource limits, OpenAI request typing, and React Compiler adoption in the stateful dashboard/viewer roots. | Parallel read-only source/config/test audit against the exact snapshot; Node 24.18.0/npm 11.17.0; exact-version Next 16.2.10 bundled upgrade, Turbopack, error-boundary, and codemod guidance; TypeScript 6 backend no-emit analysis and Node import probes via a separately installed exact-version local dependency tree; `git diff --check`. No install, registry outdated/audit, full repo lint/typecheck/test/build/browser/Docker, Supabase/OpenAI, deployment, or hosted CI run; this worktree had no `node_modules`. |
-| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | c5e7b96c3 + baeced6f2 + reviewed follow-up diff | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. The full local Chromium sweep completed with one failure: the app-mode test clicked before React attached its handler. The existing handler-readiness pattern was applied; two focused rerun attempts were then lock-blocked by another registered worktree, so exact-head hosted Chromium remains the required proof. Initial exact-head hosted checks, including Production UI, passed before these final follow-ups. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. |
+| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | bb85b546e + ledger closeout | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. The full local Chromium sweep completed 236/237 with one hydration-timing failure: the app-mode test clicked before React attached its handler. The existing handler-readiness pattern was applied; focused rerun attempts were then lock-blocked by another registered worktree, so exact-head hosted Chromium remains the required proof. Initial hosted checks, including Production UI, passed before these final follow-ups. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. |
From 35d08a43b74828ec408a3d33f638059f59e0e96f Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Sun, 19 Jul 2026 00:14:09 +0800
Subject: [PATCH 12/21] fix: keep prototype favourites out of live search
---
src/components/ClinicalDashboard.tsx | 10 ++++--
.../universal-search-command-surface.tsx | 36 +++++++++++--------
src/lib/client-env.ts | 17 +++++++++
tests/favourites-demo-boundary.test.ts | 23 ++++++++++--
4 files changed, 65 insertions(+), 21 deletions(-)
diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx
index 534bb507b..f9012e2cd 100644
--- a/src/components/ClinicalDashboard.tsx
+++ b/src/components/ClinicalDashboard.tsx
@@ -36,7 +36,12 @@ import {
} from "react";
import { type DocumentDeleteResult } from "@/components/DocumentManagementActions";
import { extractSafetyFindings } from "@/lib/clinical-safety";
-import { isLocalNoAuthMode, publicUploadsEnabled, resolveClientDemoMode } from "@/lib/client-env";
+import {
+ isLocalNoAuthMode,
+ publicUploadsEnabled,
+ resolveClientDemoMode,
+ resolveUploadReadOnlyMode,
+} from "@/lib/client-env";
import { readLocalProjectIdentity, unsafeLocalProjectMessage } from "@/lib/local-project-identity";
import { isDeployedClinicalKb } from "@/lib/deployed-app";
import {
@@ -863,10 +868,9 @@ export function ClinicalDashboard({
}, [answerThreadOwnerId, authStatus]);
// Local no-auth still has private upload APIs (`canUsePrivateApis`); do not lock the
// upload drawer just because `resolveClientDemoMode` treats no-auth as demo for favourites.
- const uploadReadOnlyMode = resolveClientDemoMode({
+ const uploadReadOnlyMode = resolveUploadReadOnlyMode({
explicitDemoMode,
authUnavailableFallback: browserAuthUnavailableDemoFallback,
- localNoAuthMode: false,
});
const localDevCanAttemptPrivateApis = process.env.NODE_ENV !== "production" && hasReadyPublicSearchSetup(setupChecks);
const canUsePublicSearchApis = localProjectReady && hasReadyPublicSearchSetup(setupChecks);
diff --git a/src/components/clinical-dashboard/universal-search-command-surface.tsx b/src/components/clinical-dashboard/universal-search-command-surface.tsx
index 62d364d20..021c80f2b 100644
--- a/src/components/clinical-dashboard/universal-search-command-surface.tsx
+++ b/src/components/clinical-dashboard/universal-search-command-surface.tsx
@@ -76,7 +76,11 @@ type LocalFavouriteMatch = {
score: number;
};
-function rankLocalFavourites(items: FavouriteItem[], query: string): LocalFavouriteMatch[] {
+function rankLocalFavourites(
+ items: FavouriteItem[],
+ query: string,
+ includePrototypeSets: boolean,
+): LocalFavouriteMatch[] {
const normalized = query.trim().toLowerCase();
if (!normalized) return [];
const tokens = normalized.split(/\s+/).filter(Boolean);
@@ -103,18 +107,20 @@ function rankLocalFavourites(items: FavouriteItem[], query: string): LocalFavour
if ((byKey.get(key)?.score ?? -1) < score) byKey.set(key, match);
}
- for (const set of favouriteSets) {
- const text = [set.title, set.meta, set.keywords].join(" ").toLowerCase();
- if (!tokens.every((token) => text.includes(token))) continue;
- const score = set.title.toLowerCase().includes(normalized) ? 100 : 8;
- byKey.set(`set:${set.id}`, {
- id: `set:${set.id}`,
- title: set.title,
- subtitle: `${set.count} saved ${set.count === 1 ? "item" : "items"} · ${set.meta}`,
- href: `/favourites?q=${encodeURIComponent(set.title)}&run=1`,
- standalone: true,
- score,
- });
+ if (includePrototypeSets) {
+ for (const set of favouriteSets) {
+ const text = [set.title, set.meta, set.keywords].join(" ").toLowerCase();
+ if (!tokens.every((token) => text.includes(token))) continue;
+ const score = set.title.toLowerCase().includes(normalized) ? 100 : 8;
+ byKey.set(`set:${set.id}`, {
+ id: `set:${set.id}`,
+ title: set.title,
+ subtitle: `${set.count} saved ${set.count === 1 ? "item" : "items"} · ${set.meta}`,
+ href: `/favourites?q=${encodeURIComponent(set.title)}&run=1`,
+ standalone: true,
+ score,
+ });
+ }
}
return [...byKey.values()].sort((left, right) => right.score - left.score || left.title.localeCompare(right.title));
@@ -429,8 +435,8 @@ export function UniversalSearchCommandSurface({
[demoMode, savedRegistryFavourites],
);
const favouriteMatches = useMemo(
- () => rankLocalFavourites(allFavouriteItems, trimmedQuery),
- [allFavouriteItems, trimmedQuery],
+ () => rankLocalFavourites(allFavouriteItems, trimmedQuery, demoMode),
+ [allFavouriteItems, demoMode, trimmedQuery],
);
const savedHrefs = useMemo(() => new Set(allFavouriteItems.map((item) => item.href)), [allFavouriteItems]);
diff --git a/src/lib/client-env.ts b/src/lib/client-env.ts
index 27009bae6..cea8c53cf 100644
--- a/src/lib/client-env.ts
+++ b/src/lib/client-env.ts
@@ -17,6 +17,23 @@ export function resolveClientDemoMode({
return explicitDemoMode || (environment !== "production" && (authUnavailableFallback || localNoAuthMode));
}
+export function resolveUploadReadOnlyMode({
+ explicitDemoMode,
+ authUnavailableFallback,
+ environment = process.env.NODE_ENV,
+}: {
+ explicitDemoMode: boolean;
+ authUnavailableFallback: boolean;
+ environment?: string;
+}) {
+ return resolveClientDemoMode({
+ explicitDemoMode,
+ authUnavailableFallback,
+ localNoAuthMode: false,
+ environment,
+ });
+}
+
export function publicUploadsEnabled() {
return process.env.NEXT_PUBLIC_PUBLIC_UPLOADS_ENABLED === "true";
}
diff --git a/tests/favourites-demo-boundary.test.ts b/tests/favourites-demo-boundary.test.ts
index 8b21bc5d7..43e74e432 100644
--- a/tests/favourites-demo-boundary.test.ts
+++ b/tests/favourites-demo-boundary.test.ts
@@ -1,7 +1,7 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
-import { resolveClientDemoMode } from "@/lib/client-env";
+import { resolveClientDemoMode, resolveUploadReadOnlyMode } from "@/lib/client-env";
const routeSource = readFileSync(new URL("../src/app/favourites/page.tsx", import.meta.url), "utf8");
const librarySource = readFileSync(
@@ -42,6 +42,8 @@ describe("favourites demo-data boundary", () => {
expect(globalShellSource).toContain("demoMode={clientDemoMode}");
expect(universalSearchSource).toContain("...(demoMode ? favouriteItems : [])");
expect(universalSearchSource).not.toContain("[...favouriteItems, ...savedRegistryFavourites]");
+ expect(universalSearchSource).toContain("if (includePrototypeSets)");
+ expect(universalSearchSource).toContain("rankLocalFavourites(allFavouriteItems, trimmedQuery, demoMode)");
});
it("fails closed in production while preserving explicit and non-production demo modes", () => {
@@ -101,8 +103,23 @@ describe("favourites demo-data boundary", () => {
environment: "development",
}),
).toBe(false);
- expect(dashboardSource).toContain("localNoAuthMode: false");
- expect(dashboardSource).toMatch(/const uploadReadOnlyMode = resolveClientDemoMode\(\{/);
+ const browserAuthUnavailableDemoFallback = true;
+ expect(
+ resolveUploadReadOnlyMode({
+ explicitDemoMode: false,
+ authUnavailableFallback: browserAuthUnavailableDemoFallback,
+ environment: "development",
+ }),
+ ).toBe(true);
+ expect(
+ resolveUploadReadOnlyMode({
+ explicitDemoMode: false,
+ authUnavailableFallback: false,
+ environment: "development",
+ }),
+ ).toBe(false);
+ expect(dashboardSource).toMatch(/const uploadReadOnlyMode = resolveUploadReadOnlyMode\(\{/);
+ expect(dashboardSource).toContain("authUnavailableFallback: browserAuthUnavailableDemoFallback");
expect(dashboardSource).not.toMatch(/const uploadReadOnlyMode = clientDemoMode\b/);
});
});
From 8050f59bc2bfe005a094af2bd8696e7fde1dbbb9 Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Sun, 19 Jul 2026 00:20:20 +0800
Subject: [PATCH 13/21] test: update design audit boundary contracts
---
docs/branch-review-ledger.md | 2 +-
docs/design-audit-2026-07-17.md | 5 +++--
tests/audit-navigation-auth-regressions.test.ts | 4 ++--
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md
index e9b66b7c6..a07ce57f7 100644
--- a/docs/branch-review-ledger.md
+++ b/docs/branch-review-ledger.md
@@ -610,4 +610,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-17 | codex/design-audit-20260716 | 47b58ddf95826231a6057b400e7f601c403a129d + reviewed working diff | exhaustive design, accessibility, UX, interaction, route, responsive, HTML/CSS/JS/TSX, and test-coverage audit with remediation | No P0. Fixed two P1 trust/responsive defects and the confirmed P2/P3 set: false Favourites provenance/demo leakage/dead controls, Therapy phone reflow/load recovery/contrast, semantic tabs/search/filters, error focus/theme, deterministic Forms back navigation, honest unavailable actions, application dialog semantics, duplicate differential keys, skip-link target, Playwright project isolation, and route coverage. External visual-target fidelity remains unavailable. | Static 261-file/~67,944-line inventory; independent combined-diff review; focused Vitest 21/21; jsdom 24/24; architecture 6/6; accessibility browser 5/5; focused route/browser recovery; lint; TypeScript; runtime/workflow/sitemap/brand/type/icon checks; production build with 1,043 pages and client-secret scan; `git diff --check`. Canonical aggregate Vitest was Windows-runner inconclusive and full UI was not repeated after focused remediation. No provider/API checks. |
| 2026-07-18 | codex/main-merge-51278-final-20260718-late | 45fa3c6c7, 14a0a898c | merge integration of 51278a70d onto fresh origin/main | Replayed the requested historical design-audit commit onto fresh `origin/main`. Kept current-main versions for five conflicts, including the regenerated drift manifest and current schema assertions. Fixed the duplicate sitemap-generator declaration, corrected redirect-section coverage, and restored the current `Clinical Guide` UI contract. | `git diff --check` and `npm run sitemap:check` passed. `npm run test` was attempted twice but blocked by the repository-wide heavy-command lock held by a separate Playwright worktree; no provider-backed checks ran. |
| 2026-07-18 | origin/main framework and dependency modernization snapshot | 4057677c8b92a5e1d997ec44958764fa91f5d424 | parallel build/infra, backend, and frontend modernization audit | Changes requested. No P0. Confirmed two P1 defects: Supabase SSR 0.12 auth-cookie responses discard mandatory anti-cache headers, and reindex bypasses the server-only-aware TSX runner after mutation-capable setup. Five P2 blockers cover the Webpack-to-Turbopack production cutover, incomplete Railway image-build watch ownership, missing clean `next typegen`, Node 26 types over a Node 24 runtime, and App Router retry actions that reset without re-fetching. P3 removal-readiness debt remains in Zod and Next Image APIs. Manual rewrite zones are auth response ownership, bundler/CSP/artifact consumers, JSZip resource limits, OpenAI request typing, and React Compiler adoption in the stateful dashboard/viewer roots. | Parallel read-only source/config/test audit against the exact snapshot; Node 24.18.0/npm 11.17.0; exact-version Next 16.2.10 bundled upgrade, Turbopack, error-boundary, and codemod guidance; TypeScript 6 backend no-emit analysis and Node import probes via a separately installed exact-version local dependency tree; `git diff --check`. No install, registry outdated/audit, full repo lint/typecheck/test/build/browser/Docker, Supabase/OpenAI, deployment, or hosted CI run; this worktree had no `node_modules`. |
-| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | bb85b546e + ledger closeout | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. The full local Chromium sweep completed 236/237 with one hydration-timing failure: the app-mode test clicked before React attached its handler. The existing handler-readiness pattern was applied; focused rerun attempts were then lock-blocked by another registered worktree, so exact-head hosted Chromium remains the required proof. Initial hosted checks, including Production UI, passed before these final follow-ups. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. |
+| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | bb85b546e + ledger closeout | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production for both prototype items and set suggestions, separated local no-auth upload capability from Favourites demo treatment, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; the final demo/upload boundary selection passed 4/4; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. The full local Chromium sweep completed 236/237 with one hydration-timing failure: the app-mode test clicked before React attached its handler. The existing handler-readiness pattern was applied; focused rerun attempts were then lock-blocked by another registered worktree, so exact-head hosted Chromium remains the required proof. Initial hosted checks, including Production UI, passed before these final follow-ups. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. |
diff --git a/docs/design-audit-2026-07-17.md b/docs/design-audit-2026-07-17.md
index a8654592f..6a2f386c2 100644
--- a/docs/design-audit-2026-07-17.md
+++ b/docs/design-audit-2026-07-17.md
@@ -22,7 +22,7 @@ This pass deliberately built on the 2026-07-15 live design audit instead of repe
| DQA-01 | P1 | Favourites rendered the same acamprosate evidence and lithium note beneath unrelated selected items, creating false item-level provenance. | Evidence and notes now derive from the selected item; absent content has an explicit honest empty state. Copy actions use the item citation and return accessible status. | `favourites-command-library-page.tsx`, `ui-smoke.spec.ts` |
| DQA-02 | P1 | Therapy Compass used fixed desktop grid tracks wider than a phone while the global shell clipped page overflow, making several production screens unreachable at 320–390 px. | Added semantic responsive layout classes, one-column phone reflow, wrapping actions/metrics, and a contract test across all Therapy Compass screens. | `therapy-compass/*`, `therapy-compass-responsive-contract.test.ts` |
| DQA-03 | P2 | White text/icons on the dark clinical accent measured materially below accessible contrast and bypassed the existing contrast token. | Replaced hard-coded white foregrounds with `--clinical-accent-contrast`, including answer status and Therapy Compass controls. | `answer-status.tsx`, `therapy-compass/*` |
-| DQA-04 | P2 | Prototype favourites appeared as genuine saved user content in normal mode, including through universal search; an unconfigured browser auth client could also fail open to demo state in production. | Gated fixtures behind the effective client demo state and introduced a shared resolver that permits auth-unavailable fallback only outside production while retaining explicit production demo opt-in. Normal mode now shows only real saved items. | `client-env.ts`, `favourites/page.tsx`, `ClinicalDashboard.tsx`, `favourites-hub.tsx`, `global-search-shell.tsx`, `master-search-header.tsx`, `universal-search-command-surface.tsx` |
+| DQA-04 | P2 | Prototype favourites appeared as genuine saved user content in normal mode, including item and set suggestions through universal search; an unconfigured browser auth client could also fail open to demo state in production. | Gated fixture items and sets behind the effective client demo state and introduced a shared resolver that permits auth-unavailable fallback only outside production while retaining explicit production demo opt-in. Normal mode now shows only real saved items. | `client-env.ts`, `favourites/page.tsx`, `ClinicalDashboard.tsx`, `favourites-hub.tsx`, `global-search-shell.tsx`, `master-search-header.tsx`, `universal-search-command-surface.tsx` |
| DQA-05 | P2 | Favourites exposed invisible selection below `2xl`, nested/synthetic mobile card controls, no-op edit/sort actions, incomplete menu keyboard behavior, clipboard focus loss, and title-only reasons on unfocusable unavailable controls. | Kept explicit native links and separate buttons on mobile, limited selection to the visible workspace, implemented real citation copy with status/focus restoration and complete menu keyboard behavior, and made unfinished actions honestly `aria-disabled`, focusable, and accessibly described. | `favourites-command-library-page.tsx`, `favourites-hub.tsx`, `favourites-hub-unavailable-controls.dom.test.tsx` |
| DQA-06 | P2 | Differential presentation controls advertised editing, filtering, reorder/remove behavior, and a fixed `+2` count that did not exist. | Removed false instructions, derived the remaining count from state, and disabled unfinished controls with availability explanations while preserving the read-only comparison. | `differential-presentation-workflow-page.tsx` |
| DQA-07 | P2 | Services selection led to enabled-looking Edit/Review/View controls that did nothing, while Compare stayed unavailable without an accurate reason. A later diff briefly disabled the real Clear action. | Removed the false actions and restored Clear. During current-main integration, the later functional checklist, confidence-detail, and selected-service comparison controls were preserved instead of replacing them with disabled placeholders. | `services-navigator-page.tsx`, `ui-smoke.spec.ts` |
@@ -40,6 +40,7 @@ This pass deliberately built on the 2026-07-15 live design audit instead of repe
| DQA-19 | P2 | The colour-reference skip link targeted `#main-content`, but the route had no matching focus target; app-shell targets also suppressed their focus outline. | Added canonical focusable main-content targets with an explicit visible focus outline and browser proof of both focus transfer and computed outline style. | `reference/colour-coding/page.tsx`, `ClinicalDashboard.tsx`, `global-search-shell.tsx`, `ui-route-coverage.spec.ts` |
| DQA-20 | P3 | Answer sheets overrode the semantic backdrop token and duplicate mobile CSS retained obsolete layout overrides. | Removed call-site backdrop overrides and redundant CSS so theme, forced-colour, and responsive behavior come from the shared tokens/layout rules. | `answer-result-surface.tsx`, `globals.css`, `ui-overlay-css-contract.test.ts` |
| DQA-21 | P2 | Route-handler redirects inherited the production server's `0.0.0.0` bind origin, so Chromium rejected Applications, Medications, and differential-presentation redirects with `ERR_ADDRESS_INVALID`. | Return same-origin relative `Location` headers, preserving allowed query state without leaking the server bind address into browser navigation. | `applications/route.ts`, `medications/route.ts`, `differentials/presentations/route.ts`, route tests |
+| DQA-22 | P2 | Local no-auth mode reused the broad Favourites demo flag for uploads, so a development session with working private upload APIs incorrectly presented the upload workspace as read-only. | Added a dedicated upload-read-only resolver that retains explicit demo and unavailable-auth lockout while excluding local no-auth treatment; focused behavior and source contracts protect the boundary. | `client-env.ts`, `ClinicalDashboard.tsx`, `favourites-demo-boundary.test.ts`, `audit-navigation-auth-regressions.test.ts` |
## Route and component interaction proof
@@ -99,5 +100,5 @@ Broad-gate limitations:
- Final branch/worktree: `codex/design-audit-final-pr-20260718` in `C:\Dev\Apps\Database\worktrees\design-audit-final-pr-20260718`.
- The current-main merge preserved the routed Therapy workspace, functional Services checklist/confidence/comparison controls, and dialog-first tool cards. Stale tests for the removed legacy wrappers/placeholders were reconciled without restoring inaccessible or false UI.
- The local PR plan passed runtime, changed-file formatting, full ESLint, TypeScript, 309 Vitest files / 2,797 tests, a webpack production build with 1,674 generated pages, client-bundle secret scanning, and 36 offline RAG golden fixtures across 21 suites. The build was rerun narrowly after an empty `NEXT_PUBLIC_DEMO_MODE` test-shell value was removed; no source change was required for that environment-only failure.
-- The exact-branch Chromium run passed 230/237 tests on its first pass. Focused production recovery passed Therapy navigation and all three relative redirect journeys; Services and Tools then passed against the identity-verified provider-free branch server. The guest upload journey was proven in production through the Source Library and exact admin-only notice; its final strict alert locator was narrowed to exclude Next's empty route announcer. A third duplicate production build was not launched under the recovery retry limit, so hosted exact-head UI remains the final confirmation for that selector.
+- The exact-branch Chromium run passed 230/237 tests on its first pass. Focused production recovery passed Therapy navigation and all three relative redirect journeys; Services and Tools then passed against the identity-verified provider-free branch server. The guest upload journey was proven in production through the Source Library and exact admin-only notice; its final strict alert locator was narrowed to exclude Next's empty route announcer. A later full sweep passed 236/237; the sole failure clicked the app-mode trigger before React attached its handler, so the test now uses the existing handler-readiness guard. Two focused rerun attempts were shared-lock blocked, making hosted exact-head UI the final confirmation.
- No OpenAI, Supabase, production-data, deployment, or live clinical workflow was invoked. Browser/API fixtures used the repository's offline environment and blocked external requests.
diff --git a/tests/audit-navigation-auth-regressions.test.ts b/tests/audit-navigation-auth-regressions.test.ts
index 0dc1fb7b0..e9c50ae9d 100644
--- a/tests/audit-navigation-auth-regressions.test.ts
+++ b/tests/audit-navigation-auth-regressions.test.ts
@@ -95,8 +95,8 @@ describe("audit navigation and auth regressions", () => {
"const uploadReadOnlyMode =",
"const canUsePrivateApis =",
);
- expect(uploadReadOnlyContract).toContain("const uploadReadOnlyMode = resolveClientDemoMode({");
- expect(uploadReadOnlyContract).toContain("localNoAuthMode: false");
+ expect(uploadReadOnlyContract).toContain("const uploadReadOnlyMode = resolveUploadReadOnlyMode({");
+ expect(uploadReadOnlyContract).toContain("authUnavailableFallback: browserAuthUnavailableDemoFallback");
expect(uploadReadOnlyContract).not.toMatch(/const uploadReadOnlyMode = clientDemoMode\b/);
const privateCapabilityContract = sourceSegment(
From 7565d818ddb1d5e592cb5fa79f5145fc293e9d6a Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 18 Jul 2026 16:22:49 +0000
Subject: [PATCH 14/21] test: tighten uploadReadOnlyMode contract for local
no-auth
Assert the dashboard uses resolveUploadReadOnlyMode without localNoAuthMode
and that the helper hardcodes localNoAuthMode: false so demo remains
upload-locked while local no-auth stays writable.
Co-authored-by: BigSimmo
---
tests/audit-navigation-auth-regressions.test.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/tests/audit-navigation-auth-regressions.test.ts b/tests/audit-navigation-auth-regressions.test.ts
index e9c50ae9d..d2aa7097e 100644
--- a/tests/audit-navigation-auth-regressions.test.ts
+++ b/tests/audit-navigation-auth-regressions.test.ts
@@ -95,9 +95,14 @@ describe("audit navigation and auth regressions", () => {
"const uploadReadOnlyMode =",
"const canUsePrivateApis =",
);
+ // Uploads stay writable in local no-auth; only explicit demo / auth-unavailable lock them.
expect(uploadReadOnlyContract).toContain("const uploadReadOnlyMode = resolveUploadReadOnlyMode({");
+ expect(uploadReadOnlyContract).toContain("explicitDemoMode,");
expect(uploadReadOnlyContract).toContain("authUnavailableFallback: browserAuthUnavailableDemoFallback");
+ expect(uploadReadOnlyContract).not.toContain("localNoAuthMode");
expect(uploadReadOnlyContract).not.toMatch(/const uploadReadOnlyMode = clientDemoMode\b/);
+ expect(uploadReadOnlyContract).not.toMatch(/const uploadReadOnlyMode = resolveClientDemoMode\b/);
+ expect(source("src/lib/client-env.ts")).toContain("localNoAuthMode: false");
const privateCapabilityContract = sourceSegment(
clinicalDashboardSource,
From 364b414ab1781ec732d8c13b27f35c989c138e97 Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Sun, 19 Jul 2026 00:38:52 +0800
Subject: [PATCH 15/21] fix: align local no-auth favourites mode
---
docs/design-audit-2026-07-17.md | 48 +++++++++++++-------------
src/app/favourites/page.tsx | 10 ++++--
tests/favourites-demo-boundary.test.ts | 5 ++-
3 files changed, 36 insertions(+), 27 deletions(-)
diff --git a/docs/design-audit-2026-07-17.md b/docs/design-audit-2026-07-17.md
index 6a2f386c2..1c1fd1054 100644
--- a/docs/design-audit-2026-07-17.md
+++ b/docs/design-audit-2026-07-17.md
@@ -17,30 +17,30 @@ This pass deliberately built on the 2026-07-15 live design audit instead of repe
## Findings and fixes
-| ID | Severity | Detailed issue and user impact | Implemented solution | Principal files |
-| ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| DQA-01 | P1 | Favourites rendered the same acamprosate evidence and lithium note beneath unrelated selected items, creating false item-level provenance. | Evidence and notes now derive from the selected item; absent content has an explicit honest empty state. Copy actions use the item citation and return accessible status. | `favourites-command-library-page.tsx`, `ui-smoke.spec.ts` |
-| DQA-02 | P1 | Therapy Compass used fixed desktop grid tracks wider than a phone while the global shell clipped page overflow, making several production screens unreachable at 320–390 px. | Added semantic responsive layout classes, one-column phone reflow, wrapping actions/metrics, and a contract test across all Therapy Compass screens. | `therapy-compass/*`, `therapy-compass-responsive-contract.test.ts` |
-| DQA-03 | P2 | White text/icons on the dark clinical accent measured materially below accessible contrast and bypassed the existing contrast token. | Replaced hard-coded white foregrounds with `--clinical-accent-contrast`, including answer status and Therapy Compass controls. | `answer-status.tsx`, `therapy-compass/*` |
-| DQA-04 | P2 | Prototype favourites appeared as genuine saved user content in normal mode, including item and set suggestions through universal search; an unconfigured browser auth client could also fail open to demo state in production. | Gated fixture items and sets behind the effective client demo state and introduced a shared resolver that permits auth-unavailable fallback only outside production while retaining explicit production demo opt-in. Normal mode now shows only real saved items. | `client-env.ts`, `favourites/page.tsx`, `ClinicalDashboard.tsx`, `favourites-hub.tsx`, `global-search-shell.tsx`, `master-search-header.tsx`, `universal-search-command-surface.tsx` |
-| DQA-05 | P2 | Favourites exposed invisible selection below `2xl`, nested/synthetic mobile card controls, no-op edit/sort actions, incomplete menu keyboard behavior, clipboard focus loss, and title-only reasons on unfocusable unavailable controls. | Kept explicit native links and separate buttons on mobile, limited selection to the visible workspace, implemented real citation copy with status/focus restoration and complete menu keyboard behavior, and made unfinished actions honestly `aria-disabled`, focusable, and accessibly described. | `favourites-command-library-page.tsx`, `favourites-hub.tsx`, `favourites-hub-unavailable-controls.dom.test.tsx` |
-| DQA-06 | P2 | Differential presentation controls advertised editing, filtering, reorder/remove behavior, and a fixed `+2` count that did not exist. | Removed false instructions, derived the remaining count from state, and disabled unfinished controls with availability explanations while preserving the read-only comparison. | `differential-presentation-workflow-page.tsx` |
-| DQA-07 | P2 | Services selection led to enabled-looking Edit/Review/View controls that did nothing, while Compare stayed unavailable without an accurate reason. A later diff briefly disabled the real Clear action. | Removed the false actions and restored Clear. During current-main integration, the later functional checklist, confidence-detail, and selected-service comparison controls were preserved instead of replacing them with disabled placeholders. | `services-navigator-page.tsx`, `ui-smoke.spec.ts` |
-| DQA-08 | P2 | Evidence records with no URL were still keyboard-focusable `href="#"` links announced as “Open source”. | Missing-source rows now render as non-links with unavailable semantics; only valid destinations render a Link. | `visual-evidence.tsx`, `visual-evidence-tabs.dom.test.tsx` |
-| DQA-09 | P2 | Therapy cards presented an enabled Favourite button with no handler or state change. | The unavailable feature is now disabled and labelled as coming soon, preventing false save confirmation. | `therapy-card.tsx` |
-| DQA-10 | P2 | Route/global error fallbacks did not reliably move focus or announce a route failure; the global fallback was hard-coded light-only. | Added programmatic heading focus and alert announcement, and made the self-contained global fallback system-colour, dark-mode, and forced-colour aware. | `route-error-boundary.tsx`, `global-error.tsx`, route error tests |
-| DQA-11 | P2 | Clinical Notes, Visual Evidence, and upload/index tab interfaces lacked complete roving keyboard behavior and reciprocal tab/panel relationships. | Added stable unique IDs, `aria-controls`/`aria-labelledby`, roving `tabIndex`, and Left/Right/Home/End auto-activation. Upload/indexing uses tab panels only with its visible mobile tablist and named regions when desktop renders every section. | `ClinicalDashboard.tsx`, `answer-result-surface.tsx`, `visual-evidence.tsx`, UI/DOM tests |
-| DQA-12 | P2 | Universal search visually highlighted an item without exposing it through `aria-activedescendant`. | The combobox now points to the active option and tests verify the relationship through keyboard navigation. | `master-search-header.tsx`, `universal-search-command-surface.tsx`, `ui-universal-search.spec.ts` |
-| DQA-13 | P2 | Differential filters and several selector groups behaved as toggles but lacked grouped/pressed semantics; the Tools result set retained orphaned tab-panel semantics after its controls became pressed filters. | Added labelled groups and stateful `aria-pressed` semantics, and exposed Tools results as a labelled group, with browser accessibility assertions. | `differentials-home.tsx`, `evidence-panels.tsx`, `applications-launcher-page.tsx`, `ui-accessibility.spec.ts`, `ui-tools.spec.ts` |
-| DQA-14 | P2 | Therapy Compass silently swallowed data-load failure, rendered an empty `0`-record catalogue while loading, and still implied a fabricated `200+` catalogue count. | Added distinct loading and failure states, a Retry action, removed fabricated totals, and retained real successful/empty states. The final integration tests the routed workspace that replaced the legacy single-page wrapper. | `use-therapy-data.ts`, `workspace.tsx`, `home-screen.tsx`, `therapy-compass-data-recovery.dom.test.tsx` |
-| DQA-15 | P2 | Direct-entry Forms “Back to forms” used browser history and could leave the application. | The control now deterministically navigates to `/forms?focus=1`; a DOM regression test verifies the destination. | `form-detail-page.tsx`, `forms-back-navigation.dom.test.tsx` |
-| DQA-16 | P2 | Firefox/WebKit production projects unintentionally collected advisory `@mockup` tests; two browser helpers also retried hydration inconsistently. | Applied production matchers and `grepInvert` to all required projects and introduced one shared app-mode hydration retry helper. | `playwright.config.ts`, `playwright-app-mode.ts`, `ui-stress.spec.ts`, `ui-tools.spec.ts` |
-| DQA-17 | P2 | Application/tool cards looked like direct links but prevented navigation to open an internal detail state, obscuring the real action. | Cards are now honest dialog buttons with `aria-haspopup="dialog"`; the dialog contains the actual destination link. | `applications-launcher-page.tsx`, `ui-smoke.spec.ts` |
-| DQA-18 | P2 | Differential diagnosis streams keyed repeated cards only by visible title, producing React duplicate-key errors and unstable list identity. | Cards now use a stable href/index composite key; the route matrix listens for console errors. | `differential-stream-page.tsx`, `ui-route-coverage.spec.ts` |
-| DQA-19 | P2 | The colour-reference skip link targeted `#main-content`, but the route had no matching focus target; app-shell targets also suppressed their focus outline. | Added canonical focusable main-content targets with an explicit visible focus outline and browser proof of both focus transfer and computed outline style. | `reference/colour-coding/page.tsx`, `ClinicalDashboard.tsx`, `global-search-shell.tsx`, `ui-route-coverage.spec.ts` |
-| DQA-20 | P3 | Answer sheets overrode the semantic backdrop token and duplicate mobile CSS retained obsolete layout overrides. | Removed call-site backdrop overrides and redundant CSS so theme, forced-colour, and responsive behavior come from the shared tokens/layout rules. | `answer-result-surface.tsx`, `globals.css`, `ui-overlay-css-contract.test.ts` |
-| DQA-21 | P2 | Route-handler redirects inherited the production server's `0.0.0.0` bind origin, so Chromium rejected Applications, Medications, and differential-presentation redirects with `ERR_ADDRESS_INVALID`. | Return same-origin relative `Location` headers, preserving allowed query state without leaking the server bind address into browser navigation. | `applications/route.ts`, `medications/route.ts`, `differentials/presentations/route.ts`, route tests |
-| DQA-22 | P2 | Local no-auth mode reused the broad Favourites demo flag for uploads, so a development session with working private upload APIs incorrectly presented the upload workspace as read-only. | Added a dedicated upload-read-only resolver that retains explicit demo and unavailable-auth lockout while excluding local no-auth treatment; focused behavior and source contracts protect the boundary. | `client-env.ts`, `ClinicalDashboard.tsx`, `favourites-demo-boundary.test.ts`, `audit-navigation-auth-regressions.test.ts` |
+| ID | Severity | Detailed issue and user impact | Implemented solution | Principal files |
+| ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| DQA-01 | P1 | Favourites rendered the same acamprosate evidence and lithium note beneath unrelated selected items, creating false item-level provenance. | Evidence and notes now derive from the selected item; absent content has an explicit honest empty state. Copy actions use the item citation and return accessible status. | `favourites-command-library-page.tsx`, `ui-smoke.spec.ts` |
+| DQA-02 | P1 | Therapy Compass used fixed desktop grid tracks wider than a phone while the global shell clipped page overflow, making several production screens unreachable at 320–390 px. | Added semantic responsive layout classes, one-column phone reflow, wrapping actions/metrics, and a contract test across all Therapy Compass screens. | `therapy-compass/*`, `therapy-compass-responsive-contract.test.ts` |
+| DQA-03 | P2 | White text/icons on the dark clinical accent measured materially below accessible contrast and bypassed the existing contrast token. | Replaced hard-coded white foregrounds with `--clinical-accent-contrast`, including answer status and Therapy Compass controls. | `answer-status.tsx`, `therapy-compass/*` |
+| DQA-04 | P2 | Prototype favourites appeared as genuine saved user content in normal mode, including item and set suggestions through universal search; an unconfigured browser auth client could also fail open to demo state in production. | Gated fixture items and sets behind one production-safe demo decision across the direct server route and client shells. Auth-unavailable fallback is non-production only, local no-auth remains consistent, and explicit production demo opt-in is retained. Normal mode shows only real saved items. | `client-env.ts`, `favourites/page.tsx`, `ClinicalDashboard.tsx`, `favourites-hub.tsx`, `global-search-shell.tsx`, `master-search-header.tsx`, `universal-search-command-surface.tsx` |
+| DQA-05 | P2 | Favourites exposed invisible selection below `2xl`, nested/synthetic mobile card controls, no-op edit/sort actions, incomplete menu keyboard behavior, clipboard focus loss, and title-only reasons on unfocusable unavailable controls. | Kept explicit native links and separate buttons on mobile, limited selection to the visible workspace, implemented real citation copy with status/focus restoration and complete menu keyboard behavior, and made unfinished actions honestly `aria-disabled`, focusable, and accessibly described. | `favourites-command-library-page.tsx`, `favourites-hub.tsx`, `favourites-hub-unavailable-controls.dom.test.tsx` |
+| DQA-06 | P2 | Differential presentation controls advertised editing, filtering, reorder/remove behavior, and a fixed `+2` count that did not exist. | Removed false instructions, derived the remaining count from state, and disabled unfinished controls with availability explanations while preserving the read-only comparison. | `differential-presentation-workflow-page.tsx` |
+| DQA-07 | P2 | Services selection led to enabled-looking Edit/Review/View controls that did nothing, while Compare stayed unavailable without an accurate reason. A later diff briefly disabled the real Clear action. | Removed the false actions and restored Clear. During current-main integration, the later functional checklist, confidence-detail, and selected-service comparison controls were preserved instead of replacing them with disabled placeholders. | `services-navigator-page.tsx`, `ui-smoke.spec.ts` |
+| DQA-08 | P2 | Evidence records with no URL were still keyboard-focusable `href="#"` links announced as “Open source”. | Missing-source rows now render as non-links with unavailable semantics; only valid destinations render a Link. | `visual-evidence.tsx`, `visual-evidence-tabs.dom.test.tsx` |
+| DQA-09 | P2 | Therapy cards presented an enabled Favourite button with no handler or state change. | The unavailable feature is now disabled and labelled as coming soon, preventing false save confirmation. | `therapy-card.tsx` |
+| DQA-10 | P2 | Route/global error fallbacks did not reliably move focus or announce a route failure; the global fallback was hard-coded light-only. | Added programmatic heading focus and alert announcement, and made the self-contained global fallback system-colour, dark-mode, and forced-colour aware. | `route-error-boundary.tsx`, `global-error.tsx`, route error tests |
+| DQA-11 | P2 | Clinical Notes, Visual Evidence, and upload/index tab interfaces lacked complete roving keyboard behavior and reciprocal tab/panel relationships. | Added stable unique IDs, `aria-controls`/`aria-labelledby`, roving `tabIndex`, and Left/Right/Home/End auto-activation. Upload/indexing uses tab panels only with its visible mobile tablist and named regions when desktop renders every section. | `ClinicalDashboard.tsx`, `answer-result-surface.tsx`, `visual-evidence.tsx`, UI/DOM tests |
+| DQA-12 | P2 | Universal search visually highlighted an item without exposing it through `aria-activedescendant`. | The combobox now points to the active option and tests verify the relationship through keyboard navigation. | `master-search-header.tsx`, `universal-search-command-surface.tsx`, `ui-universal-search.spec.ts` |
+| DQA-13 | P2 | Differential filters and several selector groups behaved as toggles but lacked grouped/pressed semantics; the Tools result set retained orphaned tab-panel semantics after its controls became pressed filters. | Added labelled groups and stateful `aria-pressed` semantics, and exposed Tools results as a labelled group, with browser accessibility assertions. | `differentials-home.tsx`, `evidence-panels.tsx`, `applications-launcher-page.tsx`, `ui-accessibility.spec.ts`, `ui-tools.spec.ts` |
+| DQA-14 | P2 | Therapy Compass silently swallowed data-load failure, rendered an empty `0`-record catalogue while loading, and still implied a fabricated `200+` catalogue count. | Added distinct loading and failure states, a Retry action, removed fabricated totals, and retained real successful/empty states. The final integration tests the routed workspace that replaced the legacy single-page wrapper. | `use-therapy-data.ts`, `workspace.tsx`, `home-screen.tsx`, `therapy-compass-data-recovery.dom.test.tsx` |
+| DQA-15 | P2 | Direct-entry Forms “Back to forms” used browser history and could leave the application. | The control now deterministically navigates to `/forms?focus=1`; a DOM regression test verifies the destination. | `form-detail-page.tsx`, `forms-back-navigation.dom.test.tsx` |
+| DQA-16 | P2 | Firefox/WebKit production projects unintentionally collected advisory `@mockup` tests; two browser helpers also retried hydration inconsistently. | Applied production matchers and `grepInvert` to all required projects and introduced one shared app-mode hydration retry helper. | `playwright.config.ts`, `playwright-app-mode.ts`, `ui-stress.spec.ts`, `ui-tools.spec.ts` |
+| DQA-17 | P2 | Application/tool cards looked like direct links but prevented navigation to open an internal detail state, obscuring the real action. | Cards are now honest dialog buttons with `aria-haspopup="dialog"`; the dialog contains the actual destination link. | `applications-launcher-page.tsx`, `ui-smoke.spec.ts` |
+| DQA-18 | P2 | Differential diagnosis streams keyed repeated cards only by visible title, producing React duplicate-key errors and unstable list identity. | Cards now use a stable href/index composite key; the route matrix listens for console errors. | `differential-stream-page.tsx`, `ui-route-coverage.spec.ts` |
+| DQA-19 | P2 | The colour-reference skip link targeted `#main-content`, but the route had no matching focus target; app-shell targets also suppressed their focus outline. | Added canonical focusable main-content targets with an explicit visible focus outline and browser proof of both focus transfer and computed outline style. | `reference/colour-coding/page.tsx`, `ClinicalDashboard.tsx`, `global-search-shell.tsx`, `ui-route-coverage.spec.ts` |
+| DQA-20 | P3 | Answer sheets overrode the semantic backdrop token and duplicate mobile CSS retained obsolete layout overrides. | Removed call-site backdrop overrides and redundant CSS so theme, forced-colour, and responsive behavior come from the shared tokens/layout rules. | `answer-result-surface.tsx`, `globals.css`, `ui-overlay-css-contract.test.ts` |
+| DQA-21 | P2 | Route-handler redirects inherited the production server's `0.0.0.0` bind origin, so Chromium rejected Applications, Medications, and differential-presentation redirects with `ERR_ADDRESS_INVALID`. | Return same-origin relative `Location` headers, preserving allowed query state without leaking the server bind address into browser navigation. | `applications/route.ts`, `medications/route.ts`, `differentials/presentations/route.ts`, route tests |
+| DQA-22 | P2 | Local no-auth mode reused the broad Favourites demo flag for uploads, so a development session with working private upload APIs incorrectly presented the upload workspace as read-only. | Added a dedicated upload-read-only resolver that retains explicit demo and unavailable-auth lockout while excluding local no-auth treatment; focused behavior and source contracts protect the boundary. | `client-env.ts`, `ClinicalDashboard.tsx`, `favourites-demo-boundary.test.ts`, `audit-navigation-auth-regressions.test.ts` |
## Route and component interaction proof
diff --git a/src/app/favourites/page.tsx b/src/app/favourites/page.tsx
index 872fac7c3..eb3ff4715 100644
--- a/src/app/favourites/page.tsx
+++ b/src/app/favourites/page.tsx
@@ -1,5 +1,6 @@
import { FavouritesCommandLibraryPage } from "@/components/clinical-dashboard/favourites-command-library-page";
-import { isDemoMode } from "@/lib/env";
+import { resolveClientDemoMode } from "@/lib/client-env";
+import { isDemoMode, isLocalNoAuthMode } from "@/lib/env";
type FavouritesPageProps = {
searchParams?: Promise<{
@@ -14,8 +15,13 @@ function firstSearchParam(value: string | string[] | undefined) {
export default async function FavouritesPage({ searchParams }: FavouritesPageProps) {
const params = searchParams ? await searchParams : {};
const query = firstSearchParam(params.q)?.trim() ?? "";
+ const demoMode = resolveClientDemoMode({
+ explicitDemoMode: isDemoMode(),
+ authUnavailableFallback: false,
+ localNoAuthMode: isLocalNoAuthMode(),
+ });
// No key={query} remount: query is a pure prop, and remounting on query
// change wiped the set/type/view/sort selections when clearing a search.
- return ;
+ return ;
}
diff --git a/tests/favourites-demo-boundary.test.ts b/tests/favourites-demo-boundary.test.ts
index 43e74e432..3f987b51f 100644
--- a/tests/favourites-demo-boundary.test.ts
+++ b/tests/favourites-demo-boundary.test.ts
@@ -28,7 +28,10 @@ const mobileCardSource = librarySource.slice(
describe("favourites demo-data boundary", () => {
it("passes trusted server demo state and never merges prototype favourites into live mode unconditionally", () => {
- expect(routeSource).toContain("demoMode={isDemoMode()}");
+ expect(routeSource).toContain("const demoMode = resolveClientDemoMode({");
+ expect(routeSource).toContain("explicitDemoMode: isDemoMode(),");
+ expect(routeSource).toContain("localNoAuthMode: isLocalNoAuthMode(),");
+ expect(routeSource).toContain("demoMode={demoMode}");
expect(librarySource).toContain("...(demoMode ? prototypeFavouriteItems : [])");
expect(librarySource).not.toContain("[...prototypeFavouriteItems, ...savedRegistryFavourites]");
expect(dashboardSource).toContain("demoMode={clientDemoMode}");
From 1e5b6a347f9fd92d7e07dc53f8001baba6ae4a07 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 18 Jul 2026 17:04:24 +0000
Subject: [PATCH 16/21] Align therapy compass recovery test with merged home
copy
---
tests/therapy-compass-data-recovery.dom.test.tsx | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/tests/therapy-compass-data-recovery.dom.test.tsx b/tests/therapy-compass-data-recovery.dom.test.tsx
index 47c3d5f83..e27f35eaf 100644
--- a/tests/therapy-compass-data-recovery.dom.test.tsx
+++ b/tests/therapy-compass-data-recovery.dom.test.tsx
@@ -64,9 +64,8 @@ describe("Therapy Compass required data recovery", () => {
release(undefined);
await waitFor(() =>
- expect(screen.getByText(/Search 1 source-grounded therapy records? by/)).toBeInTheDocument(),
+ expect(screen.getByText(/Search 1 source-grounded therapy records by/)).toBeInTheDocument(),
);
- expect(screen.getByText(/Search 1 source-grounded therapy records? by/)).toBeInTheDocument();
expect(screen.queryByText(/Search 0 source-grounded therapy/)).not.toBeInTheDocument();
});
@@ -101,7 +100,7 @@ describe("Therapy Compass required data recovery", () => {
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
expect(await screen.findByRole("heading", { name: "What therapy are you looking for?" })).toBeInTheDocument();
- expect(screen.getByText(/Search 1 source-grounded therapy records? by/)).toBeInTheDocument();
+ expect(screen.getByText(/Search 1 source-grounded therapy records by/)).toBeInTheDocument();
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(6));
});
From 7410fa79e7e26e3c026a363c42dc715c61f3e96a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 18 Jul 2026 17:07:17 +0000
Subject: [PATCH 17/21] Format therapy compass recovery test
---
tests/therapy-compass-data-recovery.dom.test.tsx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/tests/therapy-compass-data-recovery.dom.test.tsx b/tests/therapy-compass-data-recovery.dom.test.tsx
index 95a60f205..67021ada5 100644
--- a/tests/therapy-compass-data-recovery.dom.test.tsx
+++ b/tests/therapy-compass-data-recovery.dom.test.tsx
@@ -63,9 +63,7 @@ describe("Therapy Compass required data recovery", () => {
release(undefined);
- await waitFor(() =>
- expect(screen.getByText(/Search 1 source-grounded therapy record by/)).toBeInTheDocument(),
- );
+ await waitFor(() => expect(screen.getByText(/Search 1 source-grounded therapy record by/)).toBeInTheDocument());
expect(screen.getByRole("heading", { name: "What therapy are you looking for?" })).toBeInTheDocument();
expect(screen.queryByText(/Search 0 source-grounded therapy/)).not.toBeInTheDocument();
});
From c4662f950c92b29efe8a5817eeef260d352d02d4 Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Sun, 19 Jul 2026 01:16:36 +0800
Subject: [PATCH 18/21] test: preserve therapy home audit contracts
---
tests/therapy-compass-data-recovery.dom.test.tsx | 2 ++
tests/therapy-compass-responsive-contract.test.ts | 6 ++++++
2 files changed, 8 insertions(+)
diff --git a/tests/therapy-compass-data-recovery.dom.test.tsx b/tests/therapy-compass-data-recovery.dom.test.tsx
index 67021ada5..e08ef1a34 100644
--- a/tests/therapy-compass-data-recovery.dom.test.tsx
+++ b/tests/therapy-compass-data-recovery.dom.test.tsx
@@ -58,6 +58,7 @@ describe("Therapy Compass required data recovery", () => {
);
expect(await screen.findByRole("status")).toHaveTextContent("Loading therapy library…");
+ expect(screen.getAllByRole("main")).toHaveLength(1);
expect(screen.queryByText(/Search 0 source-grounded therapy/)).not.toBeInTheDocument();
expect(screen.queryByRole("heading", { name: "What therapy are you looking for?" })).not.toBeInTheDocument();
@@ -65,6 +66,7 @@ describe("Therapy Compass required data recovery", () => {
await waitFor(() => expect(screen.getByText(/Search 1 source-grounded therapy record by/)).toBeInTheDocument());
expect(screen.getByRole("heading", { name: "What therapy are you looking for?" })).toBeInTheDocument();
+ expect(screen.getAllByRole("main")).toHaveLength(1);
expect(screen.queryByText(/Search 0 source-grounded therapy/)).not.toBeInTheDocument();
});
diff --git a/tests/therapy-compass-responsive-contract.test.ts b/tests/therapy-compass-responsive-contract.test.ts
index 4f12e8bcb..15e383c4c 100644
--- a/tests/therapy-compass-responsive-contract.test.ts
+++ b/tests/therapy-compass-responsive-contract.test.ts
@@ -8,6 +8,7 @@ const therapyPath = "src/components/therapy-compass";
const stylesSource = read(`${therapyPath}/styles.tsx`);
const therapyCardSource = read(`${therapyPath}/therapy-card.tsx`);
const homeSource = read(`${therapyPath}/screens/home-screen.tsx`);
+const modeHomeTemplateSource = read("src/components/mode-home-template.tsx");
const detailSource = read(`${therapyPath}/screens/detail-screen.tsx`);
const compareSource = read(`${therapyPath}/screens/compare-screen.tsx`);
const recommendSource = read(`${therapyPath}/screens/recommend-screen.tsx`);
@@ -53,6 +54,8 @@ describe("Therapy Compass responsive contract", () => {
expect(responsiveStackCount(therapyCardSource)).toBeGreaterThanOrEqual(2);
expect(homeSource).toContain("ModeHomeMain");
expect(homeSource).toContain("ModeHomeTemplate");
+ expect(modeHomeTemplateSource).toContain("sm:grid-cols-[repeat(auto-fit,minmax(15rem,1fr))]");
+ expect(modeHomeTemplateSource).toContain("sm:flex-wrap");
expect(homeSource).toContain("desktopComposerSlotId={modeHomeDesktopComposerSlotId}");
expect(homeSource).toContain("ModeHomeVerificationFooter");
expect(responsiveStackCount(detailSource)).toBeGreaterThanOrEqual(2);
@@ -105,6 +108,9 @@ describe("clinical accent contrast contract", () => {
expect(source).not.toMatch(/bg-\[color:var\(--clinical-accent\)\][^"\n]*\btext-white\b/);
expect(source).toContain("clinical-accent-contrast");
}
+ expect(homeSource).toContain("ModeHomeTemplate");
+ expect(homeSource).not.toMatch(/background:var\(--clinical-accent\);color:#(?:fff|ffffff)/i);
+ expect(homeSource).not.toMatch(/bg-\[color:var\(--clinical-accent\)\][^"\n]*\btext-white\b/);
expect(pathwaysSource).not.toContain('? "#fff" : "var(--clinical-accent)"');
expect(briefSource).not.toContain('? "#fff" : "var(--clinical-accent)"');
});
From 664c72230766827ad44a18cb17be74da7b0df8bf Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Sun, 19 Jul 2026 01:18:19 +0800
Subject: [PATCH 19/21] fix: preserve therapy loading landmark
---
src/components/therapy-compass/workspace.tsx | 12 ++++++------
tests/therapy-compass-mode-wiring.test.ts | 5 ++++-
2 files changed, 10 insertions(+), 7 deletions(-)
diff --git a/src/components/therapy-compass/workspace.tsx b/src/components/therapy-compass/workspace.tsx
index b1a731419..88cbca7d4 100644
--- a/src/components/therapy-compass/workspace.tsx
+++ b/src/components/therapy-compass/workspace.tsx
@@ -73,14 +73,14 @@ function TherapyCompassMain({
asMain: boolean;
}) {
const b = useTcBindings();
- // Home normally leaves to ModeHomeMain. On catalogue load failure the
- // home child (and its landmark) is replaced by TherapyCompassDataError, so
- // promote this shell to for the error state only.
- const useMainLandmark = asMain || Boolean(b.error);
+ // Home normally leaves to ModeHomeMain. Initial loading and load
+ // failure replace that child, so the workspace must own the landmark then.
+ const homeNeedsMainLandmark = Boolean(b.error) || (b.loading && b.therapies.length === 0);
+ const useMainLandmark = asMain || homeNeedsMainLandmark;
const Tag = useMainLandmark ? "main" : "div";
// Home padding comes from ModeHomeMain; non-home routes keep the workspace gutter.
- // Error-on-home still needs the non-home gutter so the alert isn't flush.
- const style = asMain || b.error ? s(`min-width:0;padding:32px 40px 40px;`) : s(`min-width:0;`);
+ // Loading/error-on-home still need the non-home gutter so feedback isn't flush.
+ const style = asMain || homeNeedsMainLandmark ? s(`min-width:0;padding:32px 40px 40px;`) : s(`min-width:0;`);
return (
{b.error ? : children}
diff --git a/tests/therapy-compass-mode-wiring.test.ts b/tests/therapy-compass-mode-wiring.test.ts
index 25a892be6..db1b8e1b9 100644
--- a/tests/therapy-compass-mode-wiring.test.ts
+++ b/tests/therapy-compass-mode-wiring.test.ts
@@ -59,7 +59,10 @@ describe("Therapy Compass production-mode wiring", () => {
// Home uses ModeHomeMain; workspace must not wrap home in a second .
expect(homeSrc).toMatch(/ModeHomeMain/);
expect(workspaceSrc).toMatch(/asMain=\{!isHome\}/);
- expect(workspaceSrc).toContain("const useMainLandmark = asMain || Boolean(b.error);");
+ expect(workspaceSrc).toContain(
+ "const homeNeedsMainLandmark = Boolean(b.error) || (b.loading && b.therapies.length === 0);",
+ );
+ expect(workspaceSrc).toContain("const useMainLandmark = asMain || homeNeedsMainLandmark;");
expect(workspaceSrc).toContain('const Tag = useMainLandmark ? "main" : "div"');
});
From 0be679447817b267e00ee1259b3018f6b46758da Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Sun, 19 Jul 2026 01:31:43 +0800
Subject: [PATCH 20/21] test: follow therapy home search action
---
tests/ui-route-coverage.spec.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/ui-route-coverage.spec.ts b/tests/ui-route-coverage.spec.ts
index e90cb6120..7f36c923e 100644
--- a/tests/ui-route-coverage.spec.ts
+++ b/tests/ui-route-coverage.spec.ts
@@ -224,8 +224,8 @@ test.describe("previously uncovered production routes", () => {
},
async (currentPage) => {
const search = currentPage
- .getByRole("navigation", { name: "Therapy sections" })
- .getByRole("button", { name: "Search", exact: true });
+ .getByRole("region", { name: "Common therapy searches" })
+ .getByRole("button", { name: "Anxiety in outpatient care", exact: true });
await expect(search).toBeEnabled();
await search.click();
await expect(currentPage.getByRole("heading", { name: "Therapy Search", level: 1 })).toBeVisible();
From b54e589bd45c66197e861c8ae4677317618e73a9 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Sun, 19 Jul 2026 01:32:41 +0800
Subject: [PATCH 21/21] Initial plan (#895)