From ff77cd06c47058cf5dd4d38bf725ac37aaf4894e Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:05:01 +0800 Subject: [PATCH 1/3] fix(ui): smooth scrolling interactions --- .../clinical-dashboard/mode-action-popup.tsx | 27 +++++++---- .../differentials/differentials-home-page.tsx | 1 + tests/ui-smoke.spec.ts | 45 +++++++++++++++++-- tests/ui-stress.spec.ts | 14 +++--- tests/ui-tools.spec.ts | 22 +++------ 5 files changed, 76 insertions(+), 33 deletions(-) diff --git a/src/components/clinical-dashboard/mode-action-popup.tsx b/src/components/clinical-dashboard/mode-action-popup.tsx index ddfa7a10c..2244d4603 100644 --- a/src/components/clinical-dashboard/mode-action-popup.tsx +++ b/src/components/clinical-dashboard/mode-action-popup.tsx @@ -620,16 +620,27 @@ export function ModeActionPopup({ useEffect(() => { if (!open || useSheet) return; + let placementFrame: number | null = null; + const schedulePlacementUpdate = () => { + if (placementFrame !== null) return; + placementFrame = window.requestAnimationFrame(() => { + placementFrame = null; + updatePlacement(); + }); + }; + const scrollOptions: AddEventListenerOptions = { capture: true, passive: true }; + updatePlacement(); - window.addEventListener("resize", updatePlacement); - window.addEventListener("scroll", updatePlacement, true); - window.visualViewport?.addEventListener("resize", updatePlacement); - window.visualViewport?.addEventListener("scroll", updatePlacement); + window.addEventListener("resize", schedulePlacementUpdate); + window.addEventListener("scroll", schedulePlacementUpdate, scrollOptions); + window.visualViewport?.addEventListener("resize", schedulePlacementUpdate); + window.visualViewport?.addEventListener("scroll", schedulePlacementUpdate, { passive: true }); return () => { - window.removeEventListener("resize", updatePlacement); - window.removeEventListener("scroll", updatePlacement, true); - window.visualViewport?.removeEventListener("resize", updatePlacement); - window.visualViewport?.removeEventListener("scroll", updatePlacement); + if (placementFrame !== null) window.cancelAnimationFrame(placementFrame); + window.removeEventListener("resize", schedulePlacementUpdate); + window.removeEventListener("scroll", schedulePlacementUpdate, scrollOptions); + window.visualViewport?.removeEventListener("resize", schedulePlacementUpdate); + window.visualViewport?.removeEventListener("scroll", schedulePlacementUpdate); }; }, [open, updatePlacement, useSheet]); diff --git a/src/components/differentials/differentials-home-page.tsx b/src/components/differentials/differentials-home-page.tsx index d42d2a685..1ec6d6234 100644 --- a/src/components/differentials/differentials-home-page.tsx +++ b/src/components/differentials/differentials-home-page.tsx @@ -92,6 +92,7 @@ export function DifferentialsHomePage({ query = "", autoRunSearch = false }: Dif { + if (await menu.isVisible().catch(() => false)) return; + await trigger.click(); + await expect(menu).toBeVisible({ timeout: 5_000 }); + }).toPass({ timeout: 20_000 }); const menuBox = await menu.boundingBox(); expect(menuBox).not.toBeNull(); expect(menuBox!.x).toBeGreaterThanOrEqual(0); @@ -1296,6 +1299,40 @@ test.describe("Clinical KB UI smoke coverage", () => { await expectNoPageHorizontalOverflow(page); }); + test("desktop mode action placement coalesces scroll updates per frame", async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 900 }); + await mockPrivateUnauthenticatedApi(page); + await gotoApp(page, "/"); + await waitForDemoDashboardReady(page); + + const trigger = page.getByRole("button", { name: "Open answer options" }); + await trigger.evaluate((element) => { + const originalGetBoundingClientRect = element.getBoundingClientRect.bind(element); + element.dataset.placementReadCount = "0"; + element.getBoundingClientRect = () => { + element.dataset.placementReadCount = String(Number(element.dataset.placementReadCount ?? "0") + 1); + return originalGetBoundingClientRect(); + }; + }); + + await openDailyActions(page, "Open answer options"); + await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => resolve()))); + await trigger.evaluate((element) => { + element.dataset.placementReadCount = "0"; + }); + + const placementReads = await page.evaluate(async () => { + for (let index = 0; index < 20; index += 1) { + window.dispatchEvent(new Event("scroll")); + } + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))); + const triggerElement = document.querySelector('button[aria-label="Open answer options"]'); + return Number(triggerElement?.dataset.placementReadCount ?? "0"); + }); + + expect(placementReads).toBeLessThanOrEqual(1); + }); + test("demo answer flow reaches a source-backed answer @critical", async ({ browserName, page }) => { await page.setViewportSize({ width: 390, height: 820 }); await mockDemoApi(page); diff --git a/tests/ui-stress.spec.ts b/tests/ui-stress.spec.ts index cab282b55..da2a785cb 100644 --- a/tests/ui-stress.spec.ts +++ b/tests/ui-stress.spec.ts @@ -306,13 +306,17 @@ test.describe("Clinical KB long-content stress coverage", () => { await legacyAnswerModeToggle.click(); } else { const appModeMenu = page.getByRole("button", { name: /^Mode / }); + const appModeGroup = page.getByRole("menu", { name: "Choose app mode" }); await expect(appModeMenu).toBeVisible(); - await appModeMenu.click({ force: true }); - const answerMode = page - .getByRole("menu", { name: "Choose app mode" }) - .getByRole("menuitemradio", { name: /^Answer\b/ }); + await expect(appModeMenu).toBeEnabled(); + await expect(async () => { + if (await appModeGroup.isVisible().catch(() => false)) return; + await appModeMenu.click(); + await expect(appModeGroup).toBeVisible({ timeout: 5_000 }); + }).toPass({ timeout: 20_000 }); + const answerMode = appModeGroup.getByRole("menuitemradio", { name: /^Answer\b/ }); await expect(answerMode).toBeVisible(); - await answerMode.click({ force: true }); + await answerMode.click(); await expect(page.getByRole("button", { name: "Mode Answer" })).toBeVisible(); } diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index ef26aea94..a5008be41 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -1376,22 +1376,8 @@ test.describe("Clinical KB tools launcher", () => { 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.93, directSourceCount: 1, weakSourceCount: 0 }, + documentMatches: [], + relevance: { verdict: "weak", score: 0, directSourceCount: 0, weakSourceCount: 0 }, smartPanel: {}, telemetry: { query_class: "differential_compare", retrieval_strategy: "text_fast_path" }, scope: { queryMode: "compare_guidance" }, @@ -1407,7 +1393,11 @@ test.describe("Clinical KB tools launcher", () => { const submit = page.locator('button[aria-label="Search differential presentations"]:visible'); await input.fill("acute confusion"); await expect(submit).toBeEnabled(); + const searchResponse = page.waitForResponse( + (response) => response.url().includes("/api/search") && response.request().method() === "POST", + ); await submit.click(); + await searchResponse; const compareAction = page.getByTestId("differentials-compare-selected-mobile"); const dock = page.locator("form.answer-footer-search-dock"); From 9b5f97944ca5148b196076c872e423db64620e1a Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 17 Jul 2026 15:52:41 +0800 Subject: [PATCH 2/3] docs: record scrolling cleanup review --- 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 dd7fb15b6..190f2854d 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -565,3 +565,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-15 | HEAD / main snapshot (detached review worktree) | 0c56f27a37af88a073d2bb695d2cf4c05067ff4f | comprehensive repository review | Changes requested: two P1 defects (high-risk clinical claim support can accept a different trigger condition on lexical overlap; document-mode URL auto-run loops on navigation and leaves search loading indefinitely), one P2 supply-chain guard gap (the action-pin checker accepts mutable major tags and ignores SHA-pinned major versions), and one P3 orientation-doc gap (DSM and legacy specifier routes are absent from the codebase index). No P0 found. | Node 24/npm 11 and `npm ls --depth=0`; format, runtime, lint, TypeScript, sitemap/brand/icon/type-scale, docs links/scripts, CI/action/Codex guards; coverage 259 files passed/1 skipped and 2,417 tests passed/1 skipped; offline RAG 36 fixtures plus 282 tests; production build/client-secret scan; deployment boot smoke; critical Chromium 8/10 with two reproducible document-search failures; accessibility 5/5; viewport/focus checks through 1920x1080. Provider-backed governance, quality, drift, tenancy, hosted CI, and the cross-browser matrix were blocked or skipped by policy/targeted failures. | | 2026-07-17 | codex/historical-branch-cleanup-20260717 | e36ac0c6628264c7ed6c494a597a62d0214b68f6 | branch-cleanup and historical-content recovery | Completed the pending historical cleanup: deleted 55 exact-SHA remote refs and 20 redundant local refs, removed nine clean merged worktrees, preserved every dirty, active, open-PR, or patch-unique worktree, and recovered the still-useful governance incident runbook from `codex/domain-1-governance-remediation`. Historical code changes were either tied to merged PRs or reviewed as superseded by current implementations; open PRs #699, #700, #702, and #704 remain protected. | Fresh `git fetch --prune`; GitHub PR inventory and exact commit-to-merged-PR associations; exact remote SHA rechecks before deletion; cherry-pick-aware logs; two-dot tree and branch-only-file review; Codex task-to-worktree cross-check; focused documentation validation recorded in the cleanup PR. No OpenAI, Supabase, production-data, or live clinical workflow was run. | | 2026-07-17 | PR #704 / codex/scroll-geometry-stability-20260717 | 35e74ddbd61bacc5b34f06efbd58091f092665fd | nested scroll-source review follow-up | Confirmed the outside-diff CodeRabbit finding: the standalone shell shared one intent history across main and descendant scroll containers, so a switch from a deep main offset to a near-zero nested offset could falsely reveal chrome. Scroll metrics now identify their source, source changes rebase direction and travel while preserving visibility, and unit/UI regressions cover the switch. No unresolved actionable review finding remains. | Focused Vitest 9/9; TypeScript; scoped ESLint; Prettier; `git diff --check`. Exact-head hosted CI and UI remain required after push. No Supabase/OpenAI/live-provider checks run. | +| 2026-07-17 | codex/scrolling-cleanup-20260717 | ff77cd06c + latest origin/main sync | cross-page scrolling and interaction stability review | Fixed two confirmed P2 defects: desktop action-popup placement performed synchronous geometry work for every captured scroll event, and submitted differential searches with zero document matches fell back to the home state. Placement is now coalesced per animation frame with passive scroll listeners, and submitted empty-evidence results remain visible. Hardened three popup/navigation browser helpers that reproduced hydration timing failures. No other high-confidence defect remains in the scoped diff. | Scroll-focused Chromium 28/28; final affected Chromium 5/5; source regressions 2/2; scoped ESLint; Prettier; TypeScript and production build passed before the final upstream-only sync; `git diff --check`. The aggregate local Vitest/UI runs were affected by concurrent-worktree resource contention, so exact-head hosted CI remains required before merge. No Supabase/OpenAI/live-provider checks run. | From 42a92195ee8cce6319ddb8c3f4c4495a2230ade0 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:47:11 +0800 Subject: [PATCH 3/3] test(ui): await phone header collapse geometry --- tests/ui-smoke.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 5824902ff..364f35765 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -3301,6 +3301,7 @@ test.describe("Clinical KB UI smoke coverage", () => { spacer.style.height = "2000px"; node.appendChild(spacer); }); + const expandedMainClientHeight = await main.evaluate((node) => node.clientHeight); for (const offset of [40, 80, 120, 160, 200]) { await main.evaluate((node, top) => { node.scrollTop = top; @@ -3342,6 +3343,7 @@ test.describe("Clinical KB UI smoke coverage", () => { node.scrollTop = 0; }); await expect(collapseHost).not.toHaveAttribute("data-scroll-hidden", "true"); + await expect.poll(async () => main.evaluate((node) => node.clientHeight)).toBe(expandedMainClientHeight); const visibleGeometry = await main.evaluate((node) => ({ clientHeight: node.clientHeight, maxOffset: node.scrollHeight - node.clientHeight, @@ -3350,7 +3352,9 @@ test.describe("Clinical KB UI smoke coverage", () => { node.scrollTop = top; }, visibleGeometry.maxOffset); await expect(collapseHost).toHaveAttribute("data-scroll-hidden", "true"); - await expect.poll(async () => collapseHost.getAttribute("data-scroll-hidden"), { timeout: 1_000 }).toBe("true"); + await expect + .poll(async () => main.evaluate((node) => node.clientHeight)) + .toBeGreaterThan(visibleGeometry.clientHeight); const collapsedGeometry = await main.evaluate((node) => ({ clientHeight: node.clientHeight, maxOffset: node.scrollHeight - node.clientHeight,