From 25cd25d66b998c1cab082bb136e24f19013bd4a5 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:53:43 +0800 Subject: [PATCH 1/4] test(ui): element-overlap regression coverage for the header and composer The page-overflow smoke checks only assert document-level horizontal overflow, which overlapping siblings never trigger; this bug class shipped three times (ledger under the mode pill, composer clear button over typed text, standalone-home chips under the pill). Assert visible header elements do not stack at 640-1536px and the composer clear button occupies its own slot. Co-Authored-By: Claude Fable 5 --- playwright.config.ts | 2 +- tests/ui-overlap.spec.ts | 117 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 tests/ui-overlap.spec.ts diff --git a/playwright.config.ts b/playwright.config.ts index 73b3ee1e4b..b219750d9c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -5,7 +5,7 @@ const baseURL = getPlaywrightBaseUrl(); export default defineConfig({ testDir: "./tests", - testMatch: /.*ui-(smoke|stress|accessibility|tools)\.spec\.ts/, + testMatch: /.*ui-(smoke|stress|accessibility|tools|overlap)\.spec\.ts/, timeout: 60_000, retries: process.env.CI ? 1 : 0, expect: { diff --git a/tests/ui-overlap.spec.ts b/tests/ui-overlap.spec.ts new file mode 100644 index 0000000000..89a7f20d38 --- /dev/null +++ b/tests/ui-overlap.spec.ts @@ -0,0 +1,117 @@ +import { expect, test, type Page } from "playwright/test"; + +/** + * Element-overlap regression coverage. + * + * The page-overflow smoke checks only assert document-level horizontal + * overflow, which overlapping siblings never trigger. This header bug class + * shipped three separate times (source ledger under the mode pill, the + * composer clear button over typed text, and the standalone-home status chips + * under the pill), so these tests assert directly that visible header + * elements do not stack on top of each other at any supported width, and + * that the composer clear button occupies its own slot. + */ + +const headerWidths = [640, 768, 1024, 1152, 1280, 1366, 1440, 1536] as const; + +const readySetupChecks = [ + { id: "env", label: ".env.local configured", status: "ready", detail: "Test environment ready." }, + { id: "project", label: "Clinical KB Database target", status: "ready", detail: "Test Supabase project ready." }, + { id: "schema", label: "supabase/schema.sql applied", status: "ready", detail: "Test schema ready." }, + { id: "search", label: "Search RPC and vector indexes", status: "ready", detail: "Test search schema ready." }, + { id: "openai", label: "OpenAI API key available", status: "ready", detail: "Test OpenAI ready." }, + { id: "worker", label: "npm run worker running", status: "unknown", detail: "Worker not required for UI smoke." }, +]; + +async function mockSetupStatus(page: Page) { + await page.route("**/api/setup-status**", async (route) => { + await route.fulfill({ json: { demoMode: true, checks: readySetupChecks } }); + }); +} + +async function gotoHome(page: Page) { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForLoadState("networkidle", { timeout: 15_000 }).catch(() => undefined); +} + +type OverlapReport = { count: number; overlaps: string[] }; + +async function collectHeaderOverlaps(page: Page): Promise { + return page.evaluate(() => { + const header = document.querySelector("header#search"); + if (!header) return { count: 0, overlaps: ["header#search not found"] }; + // Interactive controls plus the styled status chips (spans) that sit + // alongside them; nested elements are excluded via the contains() check. + const candidates = Array.from( + header.querySelectorAll("button, summary, a, div > span.inline-flex, div > span.grid"), + ).filter((element) => { + const rect = element.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }); + const overlaps: string[] = []; + const label = (element: Element) => + element.getAttribute("aria-label") ?? (element.textContent ?? "").trim().slice(0, 24); + for (let i = 0; i < candidates.length; i++) { + for (let j = i + 1; j < candidates.length; j++) { + const a = candidates[i].getBoundingClientRect(); + const b = candidates[j].getBoundingClientRect(); + const xOverlap = Math.min(a.right, b.right) - Math.max(a.left, b.left); + const yOverlap = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top); + const nested = candidates[i].contains(candidates[j]) || candidates[j].contains(candidates[i]); + // 4px tolerance ignores subpixel rounding and intentional edge kisses. + if (xOverlap > 4 && yOverlap > 4 && !nested) { + overlaps.push( + `"${label(candidates[i])}" overlaps "${label(candidates[j])}" by ${Math.round(Math.min(xOverlap, yOverlap))}px`, + ); + } + } + } + return { count: candidates.length, overlaps }; + }); +} + +test.describe("Header element overlap coverage", () => { + for (const width of headerWidths) { + test(`header controls do not overlap at ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: 900 }); + await mockSetupStatus(page); + await gotoHome(page); + + const report = await collectHeaderOverlaps(page); + expect(report.count, "expected at least the mode pill and one control in the header").toBeGreaterThanOrEqual(2); + expect(report.overlaps, `overlapping header elements at ${width}px`).toEqual([]); + }); + } + + for (const viewport of [ + { name: "mobile", width: 390, height: 820 }, + { name: "desktop", width: 1280, height: 900 }, + ] as const) { + test(`composer clear button does not cover typed text at ${viewport.name}`, async ({ page }) => { + await page.setViewportSize({ width: viewport.width, height: viewport.height }); + await mockSetupStatus(page); + await gotoHome(page); + + const input = page.locator('[data-testid="global-search-input"]:visible').first(); + await input.fill("Synthetic lithium monitoring guidance question"); + + const clearButton = page.locator('[aria-label="Clear search question"]:visible').first(); + await expect(clearButton).toBeVisible(); + + const geometry = await page.evaluate(() => { + const inputElement = document.querySelector('[data-testid="global-search-input"]'); + const clearElement = document.querySelector('[aria-label="Clear search question"]'); + if (!inputElement || !clearElement) return null; + const inputRect = inputElement.getBoundingClientRect(); + const clearRect = clearElement.getBoundingClientRect(); + return { inputRight: inputRect.right, clearLeft: clearRect.left }; + }); + + expect(geometry, "input and clear button must both render").not.toBeNull(); + expect( + geometry!.inputRight, + "the input must end before the clear button starts (no text under the button)", + ).toBeLessThanOrEqual(geometry!.clearLeft + 1); + }); + } +}); From dcc033e8cc7b8d5e5f8cb6dff59d531ba415e5ba Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:54:18 +0800 Subject: [PATCH 2/4] fix(ui): remove non-functional voice input controls The mic buttons in the composer, applications launcher, and document viewer had no click handler, and the forms search field drew a mic icon implying voice support that does not exist. A silent dead control in a clinical tool erodes trust; remove them until the feature is real. Also drops the now-unused Mic imports. Co-Authored-By: Claude Fable 5 --- src/components/DocumentViewer.tsx | 8 -------- src/components/applications-launcher-page.tsx | 10 ---------- .../clinical-dashboard/master-search-header.tsx | 9 --------- src/components/forms/forms-search-results-page.tsx | 4 +--- 4 files changed, 1 insertion(+), 30 deletions(-) diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index 79d6a77e08..85c982196f 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -19,7 +19,6 @@ import { Maximize2, Menu, Minimize2, - Mic, Minus, Plus, Quote, @@ -2967,13 +2966,6 @@ export function DocumentViewer({ className="min-h-[44px] min-w-0 flex-1 bg-transparent px-2 text-base font-medium text-[color:var(--text)] outline-none placeholder:text-[color:var(--text-soft)]" /> - {usesUniversalFooterStyle ?