diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77b3e8be4a..8cdc3ba63b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -379,6 +379,11 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + # Full history: design-system-adoption validates provenance.candidateSourceHead + # with `git cat-file` / `git show` against an ancestor commit. Default depth-1 + # checkouts only have HEAD, so the binding fails with "must identify an existing + # commit" even when the SHA is on the branch (Unit coverage on PR #1729). + fetch-depth: 0 persist-credentials: false - name: Setup Node and dependencies diff --git a/docs/design-system/adoption-contract.json b/docs/design-system/adoption-contract.json index fc9e4cd762..2f8b965612 100644 --- a/docs/design-system/adoption-contract.json +++ b/docs/design-system/adoption-contract.json @@ -62,7 +62,9 @@ "docs/design-system/adoption-contract.json", "docs/design-system/adoption-manifest.json", "lighthouse-budget.json", - "tests/__screenshots__/linux/provenance.json" + "scripts/generate-design-system-adoption.mjs", + "tests/__screenshots__/linux/provenance.json", + "tests/design-system-adoption.test.ts" ] } }, diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json index 151c19f916..396543b62f 100644 --- a/docs/design-system/adoption-manifest.json +++ b/docs/design-system/adoption-manifest.json @@ -73,7 +73,9 @@ "docs/design-system/adoption-contract.json", "docs/design-system/adoption-manifest.json", "lighthouse-budget.json", - "tests/__screenshots__/linux/provenance.json" + "scripts/generate-design-system-adoption.mjs", + "tests/__screenshots__/linux/provenance.json", + "tests/design-system-adoption.test.ts" ] } } diff --git a/scripts/generate-design-system-adoption.mjs b/scripts/generate-design-system-adoption.mjs index 53a2d83eea..4b47310836 100644 --- a/scripts/generate-design-system-adoption.mjs +++ b/scripts/generate-design-system-adoption.mjs @@ -54,7 +54,12 @@ const CANONICAL_VISUAL_BASELINE_POLICY = Object.freeze({ "docs/design-system/adoption-contract.json", "docs/design-system/adoption-manifest.json", "lighthouse-budget.json", + "scripts/generate-design-system-adoption.mjs", "tests/__screenshots__/linux/provenance.json", + // First adoption also ships the fixture fix that makes emptying AWAITING_BASELINE + // testable; keep it allowlisted so candidateSourceHead can stay on an existing + // main commit (required for shallow CI checkouts and squash-merge survival). + "tests/design-system-adoption.test.ts", ], }, }); diff --git a/tests/__screenshots__/linux/dashboard-shell-phone.png b/tests/__screenshots__/linux/dashboard-shell-phone.png new file mode 100644 index 0000000000..8ca71707b8 Binary files /dev/null and b/tests/__screenshots__/linux/dashboard-shell-phone.png differ diff --git a/tests/__screenshots__/linux/dashboard-shell.png b/tests/__screenshots__/linux/dashboard-shell.png new file mode 100644 index 0000000000..043bad7f08 Binary files /dev/null and b/tests/__screenshots__/linux/dashboard-shell.png differ diff --git a/tests/__screenshots__/linux/document-viewer.png b/tests/__screenshots__/linux/document-viewer.png new file mode 100644 index 0000000000..dcfb25ec29 Binary files /dev/null and b/tests/__screenshots__/linux/document-viewer.png differ diff --git a/tests/__screenshots__/linux/provenance.json b/tests/__screenshots__/linux/provenance.json new file mode 100644 index 0000000000..015809e42f --- /dev/null +++ b/tests/__screenshots__/linux/provenance.json @@ -0,0 +1,63 @@ +{ + "schemaVersion": 2, + "platform": "linux", + "runnerImage": "ubuntu-24.04", + "candidateSourceHead": "07ada91b7c29410a0c34cc43934d1a12c686b2cf", + "source": { + "kind": "hosted-ci-artifact", + "runId": "31251091603", + "artifactName": "visual-baseline-31251091603", + "candidateSourceHead": "07ada91b7c29410a0c34cc43934d1a12c686b2cf" + }, + "review": { + "status": "approved", + "reviewerType": "human", + "reviewedBy": "BigSimmo", + "reviewedAt": "2026-08-08T11:07:51Z", + "candidateSourceHead": "07ada91b7c29410a0c34cc43934d1a12c686b2cf" + }, + "candidates": [ + { + "id": "dashboard-shell", + "path": "tests/__screenshots__/linux/dashboard-shell.png", + "sha256": "dc59e0104e4c752f418db88a46acc9f54f07e22e2c6006eaa85af828031e411d", + "width": 1196, + "height": 900 + }, + { + "id": "dashboard-shell-phone", + "path": "tests/__screenshots__/linux/dashboard-shell-phone.png", + "sha256": "1bcac08d3be07a81ebf155f2502e986983a62efd24e8a3ef1f06a6d897407da7", + "width": 390, + "height": 820 + }, + { + "id": "search-results-band", + "path": "tests/__screenshots__/linux/search-results-band.png", + "sha256": "46d41e15c3b622501df9c65f67abdfbb069bb292ebb3f4ddd1a09d0ee823ae4a", + "width": 1148, + "height": 115 + }, + { + "id": "search-results-band-phone", + "path": "tests/__screenshots__/linux/search-results-band-phone.png", + "sha256": "b703eecae02a69f58bf127386653a37f06d2b7e131d2b22bb749a7f059b457ad", + "width": 366, + "height": 89 + }, + { + "id": "document-viewer", + "path": "tests/__screenshots__/linux/document-viewer.png", + "sha256": "ea3050b274c3bf81f7e6c787b79fa2803ef83c844abea409203275390451b8af", + "width": 1196, + "height": 2903 + }, + { + "id": "therapy-compass-home", + "path": "tests/__screenshots__/linux/therapy-compass-home.png", + "sha256": "e3030324162bcba8051299281475497c67a5d732c5a3f28fd7728283e1b3bf83", + "width": 1196, + "height": 868 + } + ] +} diff --git a/tests/__screenshots__/linux/search-results-band-phone.png b/tests/__screenshots__/linux/search-results-band-phone.png new file mode 100644 index 0000000000..9b8d3313b3 Binary files /dev/null and b/tests/__screenshots__/linux/search-results-band-phone.png differ diff --git a/tests/__screenshots__/linux/search-results-band.png b/tests/__screenshots__/linux/search-results-band.png new file mode 100644 index 0000000000..5d320d7d1d Binary files /dev/null and b/tests/__screenshots__/linux/search-results-band.png differ diff --git a/tests/__screenshots__/linux/therapy-compass-home.png b/tests/__screenshots__/linux/therapy-compass-home.png new file mode 100644 index 0000000000..5974ca8aa1 Binary files /dev/null and b/tests/__screenshots__/linux/therapy-compass-home.png differ diff --git a/tests/design-system-adoption.test.ts b/tests/design-system-adoption.test.ts index dc3ed90ee5..3e7b58d392 100644 --- a/tests/design-system-adoption.test.ts +++ b/tests/design-system-adoption.test.ts @@ -87,7 +87,19 @@ function setCurrentAwaitingValues(fixtureRoot: string, values: string) { ); } -function initialiseCandidateRepository(fixtureRoot: string, awaitingValues?: string) { +/** + * Seeds a fixture repository whose candidate-source commit declares the canonical + * six as awaiting a baseline — the state a capture run is taken from. + * + * The default is the explicit canonical list rather than whatever + * `tests/ui-visual-baseline.spec.ts` happens to say right now. Inheriting the live + * file made every fixture depend on the repository not having adopted its baselines + * yet, so the commit that finally empties `AWAITING_BASELINE` — the outcome this + * contract exists to permit — turned the fixture's own candidate head into "must + * contain exactly the canonical six ids" and failed the two `toEqual([])` cases. + * A fixture states its precondition; it does not borrow it from the tree under test. + */ +function initialiseCandidateRepository(fixtureRoot: string, awaitingValues: string = canonicalAwaitingValues) { git(fixtureRoot, ["init", "-q"]); git(fixtureRoot, ["config", "user.email", "fixture@example.invalid"]); git(fixtureRoot, ["config", "user.name", "Fixture"]); @@ -95,9 +107,7 @@ function initialiseCandidateRepository(fixtureRoot: string, awaitingValues?: str writeFixtureFile( fixtureRoot, "tests/ui-visual-baseline.spec.ts", - awaitingValues === undefined - ? read("tests/ui-visual-baseline.spec.ts") - : withAwaitingValues(read("tests/ui-visual-baseline.spec.ts"), awaitingValues), + withAwaitingValues(read("tests/ui-visual-baseline.spec.ts"), awaitingValues), ); return commitFixture(fixtureRoot, "candidate source"); } diff --git a/tests/playwright-settlement-contract.test.ts b/tests/playwright-settlement-contract.test.ts new file mode 100644 index 0000000000..8c3fe68009 --- /dev/null +++ b/tests/playwright-settlement-contract.test.ts @@ -0,0 +1,36 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +/** + * Ledger #093: Next streaming can leave a hidden duplicate page root. Bare + * `page.getByTestId(...)` then fails Playwright strict mode under full-suite + * load (Production UI shard 2 on PR #1729: `dsm-home-main` resolved to 2). + * + * Page-root / shell surfaces in `tests/ui-route-coverage.spec.ts` must go + * through `visibleByTestId` (or `expectSingleSettledOwner`) from + * `tests/playwright-settlement.ts`. This offline contract keeps that defect + * from returning as a red CI flake. + */ +const ROUTE_COVERAGE = "tests/ui-route-coverage.spec.ts"; + +const PAGE_ROOT_TEST_IDS = [ + "dsm-home-main", + "dsm-home-compare", + "dsm-comparison-page", + "dsm-differential-considerations-page", + "search-query-ribbon", +] as const; + +describe("playwright settlement contract (#093)", () => { + it("ui-route-coverage scopes page-root testids through visibleByTestId", () => { + const source = readFileSync(ROUTE_COVERAGE, "utf8"); + expect(source).toMatch(/import\s*\{[^}]*\bvisibleByTestId\b[^}]*\}\s*from\s*["']\.\/playwright-settlement["']/); + + for (const testId of PAGE_ROOT_TEST_IDS) { + expect(source, `${testId} must use visibleByTestId`).toContain(`visibleByTestId(currentPage, "${testId}")`); + expect(source, `${testId} must not use bare getByTestId`).not.toMatch( + new RegExp(String.raw`getByTestId\(\s*["']${testId}["']\s*\)`), + ); + } + }); +}); diff --git a/tests/ui-route-coverage.spec.ts b/tests/ui-route-coverage.spec.ts index f73edff31d..0ee46e7556 100644 --- a/tests/ui-route-coverage.spec.ts +++ b/tests/ui-route-coverage.spec.ts @@ -9,6 +9,7 @@ import { getDifferentialRecord, } from "../src/lib/differentials"; import { loadMedicationSnapshot } from "../src/lib/medication-snapshot"; +import { visibleByTestId } from "./playwright-settlement"; const routeViewports = [ { name: "desktop", width: 1280, height: 900 }, @@ -247,7 +248,7 @@ test.describe("previously uncovered production routes", () => { await expect( currentPage.getByRole("heading", { name: "Anxiety in outpatient care", level: 1, exact: true }), ).toBeVisible(); - await expect(currentPage.getByTestId("search-query-ribbon")).toBeVisible(); + await expect(visibleByTestId(currentPage, "search-query-ribbon")).toBeVisible(); // The common-search pill lands on `/therapy-compass/search`, which is the // shared `ModeNav`. It portals into the header collapse host (outside // [data-therapy-root]), so read the canvas colour from the workspace root @@ -279,11 +280,14 @@ test.describe("previously uncovered production routes", () => { page, "/dsm", async (currentPage) => { - await expect(currentPage.getByTestId("dsm-home-main")).toBeVisible(); + // Scope to the visible owner: Next streaming can leave a hidden duplicate + // page root (#093), and bare getByTestId then fails Playwright strict mode + // (Production UI shard 2 on PR #1729). + await expect(visibleByTestId(currentPage, "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"); + const compare = visibleByTestId(currentPage, "dsm-home-compare"); await expect(compare).toBeEnabled(); await compare.click(); await expect(currentPage).toHaveURL(/\/dsm\/compare$/); @@ -297,7 +301,7 @@ test.describe("previously uncovered production routes", () => { page, "/dsm/compare?ids=major-depressive-disorder,bipolar-ii-disorder", async (currentPage) => { - await expect(currentPage.getByTestId("dsm-comparison-page")).toBeVisible(); + await expect(visibleByTestId(currentPage, "dsm-comparison-page")).toBeVisible(); await expect(currentPage.getByRole("heading", { name: "Compare DSM diagnoses", level: 1 })).toBeVisible(); }, async (currentPage) => { @@ -317,7 +321,7 @@ test.describe("previously uncovered production routes", () => { page, "/dsm/diagnoses/major-depressive-disorder/differentials", async (currentPage) => { - await expect(currentPage.getByTestId("dsm-differential-considerations-page")).toBeVisible(); + await expect(visibleByTestId(currentPage, "dsm-differential-considerations-page")).toBeVisible(); await expect(currentPage.getByRole("heading", { name: "Major depressive disorder", level: 1 })).toBeVisible(); }, async (currentPage) => { diff --git a/tests/ui-visual-baseline.spec.ts b/tests/ui-visual-baseline.spec.ts index e167bcfab3..e82fb160b6 100644 --- a/tests/ui-visual-baseline.spec.ts +++ b/tests/ui-visual-baseline.spec.ts @@ -154,14 +154,7 @@ async function settle(page: Page, target: BaselineTarget): Promise { * that adds the PNG. `declares no baseline it already has` below fails if the two * ever disagree, so the list cannot rot into a permanent exemption. */ -const AWAITING_BASELINE: ReadonlySet = new Set([ - "dashboard-shell", - "dashboard-shell-phone", - "search-results-band", - "search-results-band-phone", - "document-viewer", - "therapy-compass-home", -]); +const AWAITING_BASELINE: ReadonlySet = new Set([]); /** * Where a candidate PNG is written when no baseline exists.