diff --git a/docs/search-chrome-behaviour.md b/docs/search-chrome-behaviour.md index 556e84ba35..50c2103639 100644 --- a/docs/search-chrome-behaviour.md +++ b/docs/search-chrome-behaviour.md @@ -46,7 +46,10 @@ This repo uses one shared search experience across the global shell, dashboard r the top bar is hidden; section anchors use `--document-anchor-offset`, published from the live collapse-row height plus that sticky header when the shared bar is away, instead of a fixed `scroll-mt`. Observe the shared header from the viewer; never edit it for this. The in-column section index card is `lg+` only — phones use the header - disclosure and section sheet. + disclosure and section sheet. Exactly one element may own `id="document-overview"` (the DocumentViewer overview + landing wrapper); `DocumentClinicalSummary` must not reuse that id. The phone sheet lists only present sections — + omit `source-images` when `visualCount === 0`, and do not require a "Tables and diagrams" sheet row in smoke for + the empty-images lithium demo doc. 23. Safari's status bar, collapsing address bar, and pixels outside `window.innerHeight` are native browser/system controls. Do not use negative safe-area overscan, a fixed app root, synthetic document padding, or an opaque viewport slab to make CSS appear to own those pixels. Acceptance is no contrasting **app-owned** band around the native controls, with a matching opaque root canvas. Use the labelled physical-device matrix in [phone-chrome-physical-acceptance.md](phone-chrome-physical-acceptance.md). ## Results band (`SearchResultsHeaderBand`) diff --git a/tests/document-clinical-summary.dom.test.tsx b/tests/document-clinical-summary.dom.test.tsx index b7b54064b7..4839c4d230 100644 --- a/tests/document-clinical-summary.dom.test.tsx +++ b/tests/document-clinical-summary.dom.test.tsx @@ -78,4 +78,15 @@ describe("DocumentClinicalSummary", () => { expect(screen.getByText("A structured clinical summary has not been indexed for this document yet.")).toBeVisible(); expect(screen.queryByText("Source-backed")).not.toBeInTheDocument(); }); + + it("does not claim the document-overview section id", () => { + // DocumentViewer owns id="document-overview" on the overview landing wrapper. + // A second claim here duplicates the id and fails Production UI DOM integrity. + const { container } = render( + `?page=${page}`} onPageChange={vi.fn()} />, + ); + + expect(container.querySelector("#document-overview")).toBeNull(); + expect(screen.getByTestId("document-clinical-summary")).not.toHaveAttribute("id", "document-overview"); + }); }); diff --git a/tests/document-section-nav-contract.test.ts b/tests/document-section-nav-contract.test.ts new file mode 100644 index 0000000000..c6031f75e5 --- /dev/null +++ b/tests/document-section-nav-contract.test.ts @@ -0,0 +1,67 @@ +import { readFileSync } from "node:fs"; + +import { describe, expect, it } from "vitest"; + +/** + * Cheap wiring contract for document section navigation. + * + * Production UI on PR #1311 failed twice for the same class of drift: + * 1. `DocumentClinicalSummary` and the DocumentViewer overview wrapper both + * claimed `id="document-overview"`, so `expectDomIntegrity` failed. + * 2. ui-smoke still drove the retired in-flow "Document viewer sections" nav + * and required a "Tables and diagrams" sheet row for the lithium demo doc, + * which omits that row when `visualCount === 0`. + * + * Chromium proof stays in `tests/ui-smoke.spec.ts`; this file keeps the cheap + * suite honest about the ownership and selectors that proof depends on. + */ + +const read = (relativePath: string) => readFileSync(new URL(`../${relativePath}`, import.meta.url), "utf8"); + +const documentViewerSource = read("src/components/DocumentViewer.tsx"); +const clinicalSummarySource = read("src/components/document-viewer/document-clinical-summary.tsx"); +const sectionIndexSource = read("src/components/document-viewer/section-index.ts"); +const sectionNavSource = read("src/components/document-viewer/section-nav.tsx"); +const uiSmokeSource = read("tests/ui-smoke.spec.ts"); +const lithiumDemoSource = read("src/lib/demo-data.ts"); + +describe("document section navigation ownership", () => { + it("keeps a single document-overview section id owner", () => { + // The overview landing wrapper is the scroll target. A second id on the + // clinical summary card duplicates the DOM id and fails ui-smoke integrity. + expect(documentViewerSource).toContain("id={documentOverviewSectionId}"); + expect(clinicalSummarySource).not.toMatch(/\bid=["']document-overview["']/); + expect(clinicalSummarySource).not.toContain("id={documentOverviewSectionId}"); + expect(clinicalSummarySource).toContain('data-testid="document-clinical-summary"'); + }); + + it("exposes phone section navigation through the title sheet, not an in-flow nav row", () => { + expect(documentViewerSource).toContain('data-testid="document-section-trigger"'); + expect(sectionNavSource).toContain('testId="document-section-sheet"'); + expect(documentViewerSource).not.toMatch(/Document viewer sections/); + expect(sectionNavSource).not.toMatch(/Document viewer sections/); + }); + + it("omits Tables and diagrams from the index when a ready document has no visuals", () => { + // The lithium demo document used by ui-smoke has image_count: 0. + // Requiring that label in the phone sheet will red Production UI again. + expect(sectionIndexSource).toContain("input.visualCount > 0"); + expect(lithiumDemoSource).toMatch(/id:\s*"11111111-1111-4111-8111-111111111111"[\s\S]*?image_count:\s*0/); + }); + + it("keeps document-viewer ui-smoke on the current phone section chrome", () => { + const documentViewerSmoke = uiSmokeSource.slice( + uiSmokeSource.indexOf('test("document viewer puts the PDF preview first'), + uiSmokeSource.indexOf('test("answer glass header overlays main'), + ); + + expect(documentViewerSmoke).toContain('getByTestId("document-section-trigger")'); + expect(documentViewerSmoke).toContain('getByTestId("document-section-sheet")'); + expect(documentViewerSmoke).not.toMatch( + /getByRole\(\s*["']navigation["']\s*,\s*\{\s*name:\s*["']Document viewer sections["']/, + ); + // Demo docs without visuals omit Images from the sheet; open via summary. + expect(documentViewerSmoke).toContain("openImagesDisclosure"); + expect(documentViewerSmoke).toContain('images.locator("summary")'); + }); +});