diff --git a/.claude/skills/handoff/SKILL.md b/.claude/skills/handoff/SKILL.md index f22a85d91d..38472dc230 100644 --- a/.claude/skills/handoff/SKILL.md +++ b/.claude/skills/handoff/SKILL.md @@ -28,6 +28,12 @@ force-push, or discard work. - Default: `npm run verify:pr-local` (format + cheap gate, plus build/RAG when the scope needs them). - Touched UI/routing/styling: add `npm run verify:ui`. + - Touched `src/app/`, `src/components/`, or `tests/` (or design-system adoption + inputs): run `npm run design-system:adoption:update` and stage any regenerated + `docs/design-system/adoption-manifest.json` / marked COMPONENTS/ADOPTION sections + before push. Pre-commit syncs this when hooks are installed; Cloud agents that + bypass hooks still need the explicit update or static-pr + coverage fail together + (PR #1782). - Touched Supabase env/config: `npm run check:supabase-project` (provider — confirm first). Do not claim a gate passed unless it actually ran. 4. **Commit** with a clear message. End the message with: diff --git a/.githooks/pre-commit b/.githooks/pre-commit index f92c9ab0df..2aed7f2ddc 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -23,6 +23,7 @@ matches_staged() { sync_sitemap=0 sync_inventory=0 check_module_map=0 +sync_design_system_adoption=0 if matches_staged '^(src/app/|mockups/|src/lib/(app-modes|document-flow-routes|differentials|dsm|formulation|forms|services|specifiers|therapies)\.ts$|scripts/generate-site-map\.ts$|docs/site-map\.md$)'; then sync_sitemap=1 @@ -33,7 +34,13 @@ fi if matches_staged '^(src/app/|src/lib/|supabase/schema\.sql$|docs/codebase-index\.md$)'; then check_module_map=1 fi -if [ "$sync_sitemap" = "0" ] && [ "$sync_inventory" = "0" ] && [ "$check_module_map" = "0" ]; then +# Adoption proof indexes every tests/ + src/components|/src/app import edge. +# Stale docs/design-system/adoption-manifest.json fails both static-pr and unit +# coverage (seen on PR #1782 after a DocumentViewer DOM-test change). +if matches_staged '^(src/app/|src/components/|tests/|docs/design-system/(adoption-contract|adoption-manifest)\.json$|docs/design-system/(ADOPTION|COMPONENTS)\.md$|scripts/generate-design-system-adoption\.mjs$|\.design-sync/)'; then + sync_design_system_adoption=1 +fi +if [ "$sync_sitemap" = "0" ] && [ "$sync_inventory" = "0" ] && [ "$check_module_map" = "0" ] && [ "$sync_design_system_adoption" = "0" ]; then exit 0 fi @@ -51,6 +58,10 @@ if [ "$check_module_map" = "1" ]; then module_inputs=$(printf '%s\n' "$working_files" | grep -E '^(src/app/|src/lib/|supabase/schema\.sql$)' || true) mixed_inputs=$(printf '%s\n%s\n' "$mixed_inputs" "$module_inputs" | sed '/^$/d' | sort -u) fi +if [ "$sync_design_system_adoption" = "1" ]; then + adoption_inputs=$(printf '%s\n' "$working_files" | grep -E '^(src/app/|src/components/|tests/|docs/design-system/(adoption-contract|adoption-manifest)\.json$|docs/design-system/(ADOPTION|COMPONENTS)\.md$|scripts/generate-design-system-adoption\.mjs$|\.design-sync/)' || true) + mixed_inputs=$(printf '%s\n%s\n' "$mixed_inputs" "$adoption_inputs" | sed '/^$/d' | sort -u) +fi if [ -n "$mixed_inputs" ]; then echo "[pre-commit] Documentation inputs have unstaged or untracked changes:" >&2 printf '%s\n' "$mixed_inputs" >&2 @@ -68,6 +79,9 @@ fi if [ "$check_module_map" = "1" ]; then docs_to_check="$docs_to_check docs/codebase-index.md" fi +if [ "$sync_design_system_adoption" = "1" ]; then + docs_to_check="$docs_to_check docs/design-system/adoption-manifest.json docs/design-system/COMPONENTS.md docs/design-system/ADOPTION.md" +fi # Generators rewrite whole files. Refuse before running them when a selected # output has an unstaged edit, otherwise regeneration can erase local work and @@ -108,7 +122,7 @@ fi # Re-check after dropping a task: an empty docs_to_check would make the trailing # unstaged-docs diff match every modified file in the tree. -if [ "$sync_sitemap" = "0" ] && [ "$sync_inventory" = "0" ] && [ "$check_module_map" = "0" ]; then +if [ "$sync_sitemap" = "0" ] && [ "$sync_inventory" = "0" ] && [ "$check_module_map" = "0" ] && [ "$sync_design_system_adoption" = "0" ]; then exit 0 fi @@ -123,6 +137,22 @@ fi if [ "$check_module_map" = "1" ]; then npm run docs:check-index fi +if [ "$sync_design_system_adoption" = "1" ]; then + if [ ! -f scripts/generate-design-system-adoption.mjs ]; then + if git cat-file -e "HEAD:scripts/generate-design-system-adoption.mjs" 2>/dev/null; then + echo "[pre-commit] scripts/generate-design-system-adoption.mjs was removed or renamed by the pending commit; refusing to skip adoption synchronization." >&2 + exit 1 + fi + echo "[pre-commit] scripts/generate-design-system-adoption.mjs is absent from this worktree - skipping adoption sync." >&2 + sync_design_system_adoption=0 + docs_to_check=$(printf '%s\n' $docs_to_check | grep -Ev '^docs/design-system/(adoption-manifest\.json|COMPONENTS\.md|ADOPTION\.md)$' || true) + if [ "$sync_sitemap" = "0" ] && [ "$sync_inventory" = "0" ] && [ "$check_module_map" = "0" ] && [ "$sync_design_system_adoption" = "0" ]; then + exit 0 + fi + else + npm run design-system:adoption:update + fi +fi unstaged_docs=$(git diff --name-only -- $docs_to_check) if [ -n "$unstaged_docs" ]; then diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 1d27ef0012..e261c05c7f 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -837,16 +837,19 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-09 | cursor/differentials-diagnosis-links-9f18 | 0e77e7bc0842bef4ffc045c6dbea4626152490d1 | differentials diagnosis term links | implemented exact+alias termLinks chips on diagnosis+presentation pages; vitest 58/58; verify:pr-local green | vitest differential-diagnosis-links+detail+section-nav+route; verify:pr-local; ensure spot-check | | 2026-08-09 | cursor/differentials-diagnosis-links-9f18 | 0daa9e2f9fc84e879fd661da94203568309234a6 | PR #1768 Autopilot+Bugbot review-and-fix | Merged origin/main (DIRTY was ledger+detail-page staleness; merge-tree clean). Fixed SEGMENT_SPLIT to spaced-slash only so Delirium / medical psychosis links while alcohol/benzo, DVT/PE, food/fluid stay intact. Dispositioned: Copilot termLinks ??{} + Fragment key already fixed; CodeRabbit clean-keys moot (visibleSectionItems already cleans); CodeRabbit bare-slash split rejected (clinical harm). No Bugbot findings. Threads cleared on push. Merge left to user. | vitest differential-diagnosis-links+detail+route 49/49; verify:cheap exit 0 (543 files, 5828 passed/4 skipped); verify:pr-local exit 0 (lint/typecheck/test/build/rag-fixtures); merge-tree clean vs origin/main; no provider gates | | 2026-08-09 | cursor/differentials-diagnosis-links-9f18 | f784e81bcc0b53ef76b3da07a8e81f96d9bf0c71 | pr-1768 unblock | merged origin/main onto ba590f9; merge-tree clean; DIRTY mergeability cleared; push tip follows amend with this ledger | merge-tree clean; threads resolved; auto-merge was armed | -| 2026-08-09 | claude/documentviewer-nav-convergence-oddhjx | 1395d533cb13eadc705e47f76aa9f39a7a11c058 | DocumentViewer / in-page-nav convergence (#288): non-adoption decision recorded in docs/search-chrome-behaviour.md; merged duplicated visible-element predicate into resolveVisibleElement; new convergence guard test | Converged what was duplicated; DocumentReviewer header adoption declined on the merits with four blocking reasons recorded. No contract test edited. | verify:pr-local (546/547 files, 5883 tests pass; sole failure tests/pr-handoff-stop.test.ts reproduced on pristine origin/main), verify:phone-chrome (contracts 123 pass; focused Chromium 7 pass), contract set 12 files/151 tests pass, lint, typecheck, format | +| 2026-08-09 | claude/document-viewer-phase-3-bj5k5v | 156db63f1b60f09791e426b043ea90d427b789ab | post-#1772 test simplification: replace the viewer perf source-text grep with behavioural coverage; de-literalise rail window and keyboard label assertions | PR #1777 opened. Self-review of #1772's own tests against an excessive-strictness challenge. Finding: the client-performance-boundaries grep for resolveLiveCanvasWindow / resolveRenderAheadPages / liveCanvasLimit / requestIdleCallback was not merely brittle, it was INEFFECTIVE - replacing the budget call with a hardcoded 3 leaves every identifier in the file, so it stayed green while the viewer retained three full-zoom canvases (measured both ways). Replaced by a DOM case that binds the budget (VIEWER_MAX_ZOOM at dpr 3 gives ~16.8M backing px against the 24M budget, window collapses to 1) and fails on exactly that substitution. Also exported RAIL_IMAGE_WINDOW so the rail test derives its counts (verified by tuning 6->8: all 7 still pass), and relaxed the keyboard aria-label assertions from exact prose to the key names. Pre-existing greps for disableAutoFetch / canvas.width = 0 / pageToCleanup left alone deliberately - two are now redundant but they are another author's guard. | verify:pr-local (1 pre-existing root-only failure: pr-handoff-stop #291; 5872 passed), build OK 80s + client bundle secret check, eval:rag:offline 36 golden cases / 574 tests, lint + typecheck clean. Sabotage-verified in both directions. Browser gates unrunnable here (#279) - unchanged by this diff. | | 2026-08-09 | claude/planning-build-intelligence-9ot0nm | 3df3cb3993f73cda4dbbc4ac7549f84b3c6ea7ed | Node 24.15 engine floor: engines.node, preinstall hook, check:runtime, session-start provisioning, codex-cloud assertion | Authored and handed off as PR #1771; closes #285; operationalRisk true, clinicalRisk/ragRanking false | test 5800 passed/1 pre-existing root-uid failure (pr-handoff-stop, confirmed on stashed clean tree); lint 0; typecheck 0; prettier --check . pass; check:runtime pass; check:codex-cloud pass; check:outstanding-issues pass; preinstall boundary proof 24.13/24.14.9 reject, 24.15/24.19 accept, 25.0.0 reject; contract test mutation-checked red | | 2026-08-09 | pull/1771 | 466ec4216272c31c5f754db213dbdc529583b167 | PR 1771 runtime floor enforcement | P2: Cloud and Desktop setup paths remain major-only; do not merge until range-aware | static review; check:runtime PASS; check:codex-cloud PASS; ledger PASS; outstanding issues PASS; focused Vitest blocked by active Playwright lease | -| 2026-08-09 | claude/document-viewer-phase-3-bj5k5v | 156db63f1b60f09791e426b043ea90d427b789ab | post-#1772 test simplification: replace the viewer perf source-text grep with behavioural coverage; de-literalise rail window and keyboard label assertions | PR #1777 opened. Self-review of #1772's own tests against an excessive-strictness challenge. Finding: the client-performance-boundaries grep for resolveLiveCanvasWindow / resolveRenderAheadPages / liveCanvasLimit / requestIdleCallback was not merely brittle, it was INEFFECTIVE - replacing the budget call with a hardcoded 3 leaves every identifier in the file, so it stayed green while the viewer retained three full-zoom canvases (measured both ways). Replaced by a DOM case that binds the budget (VIEWER_MAX_ZOOM at dpr 3 gives ~16.8M backing px against the 24M budget, window collapses to 1) and fails on exactly that substitution. Also exported RAIL_IMAGE_WINDOW so the rail test derives its counts (verified by tuning 6->8: all 7 still pass), and relaxed the keyboard aria-label assertions from exact prose to the key names. Pre-existing greps for disableAutoFetch / canvas.width = 0 / pageToCleanup left alone deliberately - two are now redundant but they are another author's guard. | verify:pr-local (1 pre-existing root-only failure: pr-handoff-stop #291; 5872 passed), build OK 80s + client bundle secret check, eval:rag:offline 36 golden cases / 574 tests, lint + typecheck clean. Sabotage-verified in both directions. Browser gates unrunnable here (#279) - unchanged by this diff. | -| 2026-08-09 | claude/disabled-button-accessibility-piclvr | 722abdb780c715c0a89df268ed48f6c741ffd569 | disabled-placeholder buttons -> aria-disabled + inert handler (25 sites, 13 components); controlDisabled/therapy recipe aria-disabled styling; require-button-wiring redundantDisabledPair gate; wiring-conventions contract rewrite (settles #291) | authored — PR #1778 opened | lint (uncached, exit 0); typecheck; test 5878 passed/1 pre-existing root-env failure in pr-handoff-stop; build; check:rag:fixtures 36 golden cases; prettier --check clean; verify:ui not run (no browser in container) | -| 2026-08-09 | claude/in-page-nav-pr-3-i6gi8n | 6651feef4fab63f1181fba57908cb22e2932df3c | in-page-nav PR 3: convert /medications/[slug] (panel-swap) and /factsheets/[slug] (anchors) onto InPageNavHeader; record the differentials-presentations exception; delete orphaned SecondaryNavigation (#271) | converted 2 of 3 routes, 3rd recorded as a reasoned lasting exception; tocFor and SecondaryNavigation deleted; route-sections contract 7 -> 12 routes plus a panel-swap suite | verify:pr-local (1 pre-existing root-permission failure in pr-handoff-stop.test.ts, all else green); test 5932 passed; in-page-nav-route-sections 29 passed; verify:phone-chrome 3/4 stages (focused-browser blocked by #255 Chromium 1194 vs 1234); build + bundle-budget + rag:fixtures green; verify:ui not run (#255, delegated to CI) | +| 2026-08-09 | PR #1782 / cursor/fix-document-open-scroll-e5bf | 5709f2cc7a954197e02107c96d7896d8d13445c3 | document-viewer open-at-top | ship: remove chunk mount scrollIntoView so document opens stay at overview top | document-viewer-shell.dom 7 pass; document-section-summary.dom 8 pass; verify:pr-local dry-run | | 2026-08-09 | claude/m3-token-debt-262-261 | c6e1fe7fc42ec6f286eb5a3d8f7ddad7dfad2724 | design-system contract: raw padding/radius/line-height ratchets + type-step selection gate (#262 parts 2/3); closed #218/#270 | Authored and self-verified; PR #1780 open, auto-merge deliberately not armed (gate change). Baseline additive: all 15 pre-existing metrics and every debtByPath entry byte-identical; 94/94 new findings verified present at their cited line. Mutation-tested both halves of part 3 and three failure modes of part 2. | check:design-system-contract, check:icon-scale, check:type-scale, check:outstanding-issues, vitest design-system-contract-utils (31 passed), format:check whole-tree, verify:cheap (exit 1 from 5 pre-existing failures, none in this diff; 3 cleared by merging main, remaining 2 byte-identical to origin/main) | +| 2026-08-09 | claude/documentviewer-nav-convergence-oddhjx | 1395d533cb13eadc705e47f76aa9f39a7a11c058 | DocumentViewer / in-page-nav convergence (#288): non-adoption decision recorded in docs/search-chrome-behaviour.md; merged duplicated visible-element predicate into resolveVisibleElement; new convergence guard test | Converged what was duplicated; DocumentReviewer header adoption declined on the merits with four blocking reasons recorded. No contract test edited. | verify:pr-local (546/547 files, 5883 tests pass; sole failure tests/pr-handoff-stop.test.ts reproduced on pristine origin/main), verify:phone-chrome (contracts 123 pass; focused Chromium 7 pass), contract set 12 files/151 tests pass, lint, typecheck, format | +| 2026-08-09 | claude/disabled-button-accessibility-piclvr | 722abdb780c715c0a89df268ed48f6c741ffd569 | disabled-placeholder buttons -> aria-disabled + inert handler (25 sites, 13 components); controlDisabled/therapy recipe aria-disabled styling; require-button-wiring redundantDisabledPair gate; wiring-conventions contract rewrite (settles #291) | authored — PR #1778 opened | lint (uncached, exit 0); typecheck; test 5878 passed/1 pre-existing root-env failure in pr-handoff-stop; build; check:rag:fixtures 36 golden cases; prettier --check clean; verify:ui not run (no browser in container) | | 2026-08-09 | cursor/differentials-four-page-nav-5ebf | 93ea437610c1f1b681c3a5cbdc72fe8b9b178710 | differentials four-page nav | implemented Search/Diagnoses/Presentations/Compare equal pages; compare queue; kind labels; Search q+run restore | vitest nav+differentials-navigation; typecheck; lint; full unit 5814 passed | | 2026-08-09 | cursor/differentials-four-page-nav-5ebf | 384a1bedd8dd1064fb2fcf26ac845224e2cafdc4 | PR #1774 differentials four-page nav heavy review-and-fix | fixed P1 ids+Playwright; ModeNav route gate; RSC queue clears bundle+shadow; Copilot ModeNav-on-detail dispositioned (info page); ledger reorder dispositioned (merge=ledger) | vitest nav 47p; design-system-contract; typecheck; lint; test 5897p; build+bundle-budget 1543.7 within tol; focused pw compare queue 1p | | 2026-08-09 | claude/m3-token-debt-262-261 | 95221ef4235abd9544158b07b8b8569f00c9ec78 | PR #1780 review-and-fix | fixed P2 ratchet bypasses (arbitrary-property classes, CSS-consumer exemption anti-rot, modern CSS zero units); Bugbot clean; merge-tree clean; required CI was green on prior tip | vitest design-system-contract-utils 32/32; check:design-system-contract; mutation CSS-exemption fail→restore; verify:cheap PASS (549 files / 5933 tests); verify:pr-local stages PASS (test flake in design-system-adoption timed out once then 51/51 + full test 549/549 + check:rag:fixtures PASS); no provider gates | | 2026-08-09 | claude/m3-token-debt-262-261 | 7bac3bd762b381cb25c9b2a15ef3bb7223d15b16 | PR #1780 review-and-fix | fixed P2 ratchet bypasses (arbitrary-property classes, CSS-consumer exemption anti-rot, modern CSS zero units); Bugbot clean; merge-tree clean; required CI was green on prior tip | vitest design-system-contract-utils 32/32; check:design-system-contract; mutation CSS-exemption fail→restore; verify:cheap PASS (549 files / 5933 tests); verify:pr-local stages PASS (test flake in design-system-adoption timed out once then 51/51 + full test 549/549 + check:rag:fixtures PASS); no provider gates | | 2026-08-09 | cursor/dsm-search-header-fix-15d6 | df088c766f1761496189ec09146aa54c23b1c012 | dsm-search-header | pass: removed catalogue page strip; ribbon + category filter match target | vitest dsm-search-empty-state; npm test 5857 passed; lint; typecheck; ensure phone /dsm/search?q=Delirium | | 2026-08-09 | claude/m3-token-debt-262-261 | fe75e6acade008e68f953e235cc035f2e5d9d216 | PR #1780 review-and-fix | fixed P2 ratchet bypasses; synced origin/main (#1775); Bugbot clean; merge-tree clean | vitest design-system-contract-utils 32/32; check:design-system-contract; mutation CSS-exemption; verify:cheap PASS 549/5933; verify:pr-local stages PASS after adoption flake retest; check:rag:fixtures PASS; no provider gates | +| 2026-08-09 | cursor/fix-document-open-scroll-e5bf (PR #1782) | 98029875db7d640d3e699829249bb33892296bff | PR #1782 unblock | before: static-pr+coverage failed on stale adoption-manifest (document-viewer-shell testFiles drift), merge-tree clean 0 behind, auto-merge armed, 1 advisory CodeRabbit waitFor thread; after: regenerated adoption-manifest, hardened scroll negative assertion, pre-commit+handoff adoption sync to prevent recurrence; CodeRabbit dispositioned as fixed by sync assert | check:design-system-adoption PASS; vitest design-system-adoption+document-viewer-shell+docs-inventory 63/63 PASS; format; no provider-backed checks | +| 2026-08-09 | claude/in-page-nav-pr-3-i6gi8n | 6651feef4fab63f1181fba57908cb22e2932df3c | in-page-nav PR 3: convert /medications/[slug] (panel-swap) and /factsheets/[slug] (anchors) onto InPageNavHeader; record the differentials-presentations exception; delete orphaned SecondaryNavigation (#271) | converted 2 of 3 routes, 3rd recorded as a reasoned lasting exception; tocFor and SecondaryNavigation deleted; route-sections contract 7 -> 12 routes plus a panel-swap suite | verify:pr-local (1 pre-existing root-permission failure in pr-handoff-stop.test.ts, all else green); test 5932 passed; in-page-nav-route-sections 29 passed; verify:phone-chrome 3/4 stages (focused-browser blocked by #255 Chromium 1194 vs 1234); build + bundle-budget + rag:fixtures green; verify:ui not run (#255, delegated to CI) | +| 2026-08-09 | cursor/fix-document-open-scroll-e5bf (PR #1782) | 86698228533ebe10452c10c1bd7a3e1610d891ae | PR #1782 unblock | merged origin/main (behind-but-clean); fixed static-pr TS2322 on document-viewer-shell chunk fixture; fixed Production UI DSM compare remove stall via location.assign + DOM proof; prior adoption-manifest drift already fixed | tsc clean for changed files; vitest document-viewer-shell+dsm-compare-remove+design-system-adoption 59/59 PASS; check:design-system-adoption PASS; format; no provider-backed checks | diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json index 8e7729860b..94a1f18f25 100644 --- a/docs/design-system/adoption-manifest.json +++ b/docs/design-system/adoption-manifest.json @@ -392,6 +392,7 @@ "tests/answer-render-policy.test.ts", "tests/citations.test.ts", "tests/design-sync-visual-exports.test.ts", + "tests/document-viewer-shell.dom.test.tsx", "tests/rag-claim-support.test.ts", "tests/rag-eval-source-governance.test.ts", "tests/source-backed-recovery-cross-reference.test.ts", diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index a0aca346d6..c8b41cf574 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -970,15 +970,6 @@ export function DocumentViewer({ : typeof indexHealth?.warnings === "string" && indexHealth.warnings ? [indexHealth.warnings] : []; - useEffect(() => { - if (!activeChunkId || loadingDocument) return; - // Citation landing: keep the PDF as the first reading surface. Do not dump - // the viewport into the indexed source-passages list. - globalThis.document.getElementById("pdf-preview-section")?.scrollIntoView({ - block: "start", - behavior: resolveScrollBehavior(), - }); - }, [activeChunkId, loadingDocument]); const inspectIndexedTextSection = useCallback(() => { setInspectRevealKey(`${documentId}::${activeChunkId ?? ""}`); window.requestAnimationFrame(() => { diff --git a/src/components/dsm/dsm-compare-remove-link.tsx b/src/components/dsm/dsm-compare-remove-link.tsx index 252d710af6..b26294e29f 100644 --- a/src/components/dsm/dsm-compare-remove-link.tsx +++ b/src/components/dsm/dsm-compare-remove-link.tsx @@ -1,7 +1,6 @@ "use client"; import Link from "next/link"; -import { useRouter } from "next/navigation"; import type { ReactNode, MouseEvent } from "react"; /** @@ -9,8 +8,10 @@ import type { ReactNode, MouseEvent } from "react"; * * `` soft-nav from `/dsm/compare?ids=a,b` → `/dsm/compare?ids=b` can * complete its click handler under Production UI load without ever updating - * the URL (zero network, URL stuck). Explicit `router.push` is the same - * pattern `useResultSort` uses for pathname-stable query updates. + * the URL (zero network, URL stuck). `router.push` was the next attempt and + * still stalled under full-suite Chromium load on PR #1782: the remove + * control went `[active]` while `?ids=` stayed unchanged for 15s. A full + * assign is the durable hop for this filter change. */ export function DsmCompareRemoveLink({ href, @@ -23,11 +24,10 @@ export function DsmCompareRemoveLink({ className?: string; children: ReactNode; }) { - const router = useRouter(); - function onClick(event: MouseEvent) { event.preventDefault(); - router.push(href); + event.stopPropagation(); + window.location.assign(href); } return ( diff --git a/tests/docs-inventory.test.ts b/tests/docs-inventory.test.ts index 4364125cdf..cde7996407 100644 --- a/tests/docs-inventory.test.ts +++ b/tests/docs-inventory.test.ts @@ -54,6 +54,9 @@ describe("scripts documentation inventory", () => { expect(hook).toContain('git cat-file -e "HEAD:scripts/update-docs-inventory.mjs"'); expect(hook).toContain("was removed or renamed by the pending commit; refusing to skip"); expect(hook).toContain("npm run docs:check-index"); + expect(hook).toContain("npm run design-system:adoption:update"); + expect(hook).toContain("sync_design_system_adoption=0"); + expect(hook).toContain("docs/design-system/adoption-manifest.json"); expect(hook).toContain("sync_sitemap=0"); expect(hook).toContain("--diff-filter=ACMRD"); expect(hook).toContain("git ls-files --others --exclude-standard"); diff --git a/tests/document-viewer-shell.dom.test.tsx b/tests/document-viewer-shell.dom.test.tsx index c1b6c2da9b..7213b0c52c 100644 --- a/tests/document-viewer-shell.dom.test.tsx +++ b/tests/document-viewer-shell.dom.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; // DocumentViewer resolves a four-way shell state (loading / ready / auth-required @@ -65,7 +65,7 @@ vi.mock("@/components/document-viewer/pdf-canvas-viewer", () => ({ import { DocumentViewer } from "@/components/DocumentViewer"; import type { DocumentDetailPayload } from "@/lib/document-detail-contract"; -function detailPayload() { +function detailPayload(): DocumentDetailPayload { return { document: { id: "doc-1", @@ -101,7 +101,7 @@ function detailPayload() { }, pageWindow: { from: 1, to: 4, limit: 4, total: 4, hasBefore: false, hasAfter: false }, chunkWindow: { offset: 0, limit: 8, total: 8, hasBefore: false, hasAfter: false, selectedChunkId: null }, - } satisfies DocumentDetailPayload; + }; } // In demo / local-no-auth mode every document is public, so the private-access @@ -173,6 +173,45 @@ describe("DocumentViewer — shell states", () => { expect(screen.queryByText("Sign in required")).toBeNull(); }); + // Search/answer opens always attach ?chunk=…. Citation landing used to + // scrollIntoView(#pdf-preview-section) whenever a chunk was present, which + // skipped the phone overview at the top. Open at the top; the PDF still + // targets the cited page inside its own canvas. + it("does not auto-scroll the page to the PDF when opening with a chunk deep-link", async () => { + const scrolledIds: string[] = []; + vi.mocked(Element.prototype.scrollIntoView).mockImplementation(function scrollIntoView(this: Element) { + if (this.id) scrolledIds.push(this.id); + }); + + const detail = detailPayload(); + detail.chunks = [ + { + id: "chunk-1", + page_number: 1, + chunk_index: 0, + section_heading: "Scope", + content: "Cited passage", + image_ids: [], + metadata: {}, + }, + ]; + detail.window.selectedChunkId = "chunk-1"; + detail.window.chunks.selectedChunkId = "chunk-1"; + detail.chunkWindow.selectedChunkId = "chunk-1"; + + render(); + + expect(await screen.findByRole("heading", { level: 1, name: "Clozapine Titration Guideline" })).toBeVisible(); + expect(document.getElementById("pdf-preview-section")).not.toBeNull(); + + // Flush mount effects before the negative assertion. A waitFor that can + // pass while scrolledIds is still empty would miss a late scrollIntoView. + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + expect(scrolledIds).not.toContain("pdf-preview-section"); + }); + it("requires two characters and ignores an aborted search response after the query changes", async () => { const pendingSearches: Array<{ url: string; diff --git a/tests/dsm-compare-remove-link.dom.test.tsx b/tests/dsm-compare-remove-link.dom.test.tsx new file mode 100644 index 0000000000..6fd4715116 --- /dev/null +++ b/tests/dsm-compare-remove-link.dom.test.tsx @@ -0,0 +1,30 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { DsmCompareRemoveLink } from "@/components/dsm/dsm-compare-remove-link"; + +describe("DsmCompareRemoveLink", () => { + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it("assigns the href on click so same-route search-param removals cannot stall", () => { + const assign = vi.fn(); + vi.stubGlobal("location", { ...window.location, assign }); + + render( + + Remove + , + ); + + const link = screen.getByRole("link", { name: "Remove Major depressive disorder from comparison" }); + fireEvent.click(link); + + expect(assign).toHaveBeenCalledWith("/dsm/compare?ids=bipolar-ii-disorder"); + }); +}); diff --git a/tests/ui-route-coverage.spec.ts b/tests/ui-route-coverage.spec.ts index 277b3a7549..0d76b29314 100644 --- a/tests/ui-route-coverage.spec.ts +++ b/tests/ui-route-coverage.spec.ts @@ -307,16 +307,19 @@ test.describe("previously uncovered production routes", () => { async (currentPage) => { // Scope to the visible comparison owner (#093): under Production UI load, // Next streaming can leave a hidden duplicate root, and same-route - // search-param `` soft-nav has been observed to click without - // updating the URL. Pin the visible tree and wait for navigation with - // the click (DsmCompareRemoveLink uses router.push for this hop). + // search-param soft-nav has been observed to click without updating the + // URL. Pin the visible tree and wait for navigation with the click + // (DsmCompareRemoveLink uses location.assign for this hop). const pageRoot = visibleByTestId(currentPage, "dsm-comparison-page"); const remove = pageRoot.getByRole("link", { name: "Remove Major depressive disorder from comparison", }); await expect(remove).toBeEnabled(); await Promise.all([ - currentPage.waitForURL(/\/dsm\/compare\?ids=bipolar-ii-disorder$/, { timeout: 15_000 }), + currentPage.waitForURL(/\/dsm\/compare\?ids=bipolar-ii-disorder$/, { + timeout: 15_000, + waitUntil: "domcontentloaded", + }), remove.click(), ]); await expect(currentPage.getByRole("heading", { name: "Choose at least two diagnoses" })).toBeVisible();