From c8649faddf8ae9c1e85c3904398b7352239dc7a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 20 Oct 2026 16:45:11 +0200 Subject: [PATCH] test(ui): sharpen the page-title and empty-state guards The H1 guard matched the raw text, so seven files sat on its allowlist only for mentioning an H1 in a comment; it strips comments first now and the entries are gone. The empty-state ratchet counted against a ceiling, which let a new copy in wherever an old one left, and missed the no...Yet keys: it freezes the remaining sites by file and key, recognises no...Yet and nothing... keys, and the two it missed (the insight status card and the screener card) render EmptyState. --- src/__tests__/empty-state-ratchet.test.ts | 64 ++++++++++++------- src/__tests__/page-header-h1-guard.test.ts | 29 ++++----- .../insights/insight-status-card.tsx | 9 ++- .../mental-health/instrument-card.tsx | 10 ++- 4 files changed, 68 insertions(+), 44 deletions(-) diff --git a/src/__tests__/empty-state-ratchet.test.ts b/src/__tests__/empty-state-ratchet.test.ts index 028e945b4..7b95208ab 100644 --- a/src/__tests__/empty-state-ratchet.test.ts +++ b/src/__tests__/empty-state-ratchet.test.ts @@ -5,15 +5,17 @@ * Seventy-five surfaces said "nothing here yet" in a hand-written muted * paragraph, in five sizes and three alignments, some centred in a padded * box, some a bare line under the header. This is a ratchet, not a ban: the - * hand-written copies that remain are counted, the ceiling below may only go - * down, and a new copy fails the run. The ones that stay are not list or - * section empty states but a field that has no value ("not recorded" in a - * profile row), where a centred block would be the wrong shape. + * hand-written copies that remain are listed by file and key, a new one + * fails the run, and a site that moves over leaves the list. The ones that + * stay are not list or section empty states but a field that has no value + * ("not recorded" in a profile row), where a centred block would be the + * wrong shape. A count would let a new copy in wherever an old one left. * * Matched: a `

`, `

` or `` whose class list carries * `text-muted-foreground` and whose only child is `{t("")}` with a last * key segment that names an empty state (`empty`, `none`, `noData`, - * `noResults`, `notRecorded`, …; a `…Hint` key is a hint, not a state). + * `noResults`, `notRecorded`, `no…Yet`, `nothing…`; a `…Hint` key is a + * hint, not a state). * * Not matched (stated, not hidden): a message assembled from several nodes, * a key built at runtime, a state without the muted colour. The guard holds @@ -28,15 +30,31 @@ import { walkSourceFiles } from "./helpers/source-files"; const SRC = join(__dirname, ".."); /** - * The hand-written empty states that remain. Lower it when a site moves to - * `EmptyState`; never raise it. + * The hand-written empty states that remain, each a field-level line rather + * than a list or section state. Remove an entry when its site moves to + * `EmptyState`; never add one. */ -const CEILING = 13; +const REMAINING = [ + "components/cycle/cycle-phase-crosstab.tsx: cycle.insights.headline.empty", + "components/documents/episode-documents-card.tsx: documents.episodeCard.emptyLine", + "components/illness/episode-visits-card.tsx: encounters.episodeCard.emptyLine", + "components/insights/workout-detail/day-context.tsx: insights.workouts.detail.dayMoodNone", + "components/insights/workout-detail/day-context.tsx: insights.workouts.detail.daySleepNone", + "components/labs/biomarker-aliases.tsx: labs.aliases.empty", + "components/medications/dose-history-ledger.tsx: medications.detail.verlauf.summaryEmpty", + "components/medications/scheduling/dose-window-editor.tsx: medications.detail.zeitplan.window.noTimes", + "components/medications/sections/composition-section.tsx: medications.composition.editorEmpty", + "components/records/screening-hints-card.tsx: records.life.screening.noReminder", + "components/settings/ai/fallback-chain-card.tsx: settings.ai.providerChain.addNoneAvailable", + "components/settings/integrations/apple-health-card.tsx: settings.appleHealth.delivery.noSyncYet", + "components/settings/report-selection/scope-summary.tsx: reportSelection.scopeNone", + "components/timeline/selection-bar.tsx: timeline.selection.empty", +]; const ELEMENT_RE = /<(p|div|span)\b([^>]*?)>\s*\{t\(\s*"([a-zA-Z0-9_.]+)"[^)]*\)\}\s*<\/\1>/g; const EMPTY_KEY_RE = - /(?:^|\.)[A-Za-z0-9]*?(?:empty|Empty|none|None|noData|NoData|noResults|NoResults|notRecorded|NotRecorded|noSeries|NoSeries|noReminder|NoReminder|noMedications|noSymptoms|noTimes)[A-Za-z0-9]*$/; + /(?:^|\.)[A-Za-z0-9]*?(?:empty|Empty|none|None|noData|NoData|noResults|NoResults|notRecorded|NotRecorded|noSeries|NoSeries|noReminder|NoReminder|noMedications|noSymptoms|noTimes|nothing|Nothing)[A-Za-z0-9]*$|(?:^|\.)no[A-Z][A-Za-z0-9]*Yet$/; function findHandWrittenEmptyStates( files: { path: string; source: string }[], @@ -68,20 +86,12 @@ describe("empty states go through EmptyState (ratchet)", () => { expect(files.length).toBeGreaterThan(800); }); - it("adds no hand-written empty state", () => { - const hits = findHandWrittenEmptyStates(files); + it("adds no hand-written empty state, and lists none that moved over", () => { + const hits = findHandWrittenEmptyStates(files).sort(); expect( - hits.length, - `${hits.length} hand-written empty states, ceiling ${CEILING}. Render the new one through (or ChartEmptyState on a chart body).\n${hits.join("\n")}`, - ).toBeLessThanOrEqual(CEILING); - }); - - it("has its ceiling lowered when a site moves over", () => { - const hits = findHandWrittenEmptyStates(files); - expect( - hits.length, - `Only ${hits.length} hand-written empty states remain: lower CEILING to ${hits.length}.`, - ).toBe(CEILING); + hits, + 'A new hand-written empty state: render it through (or ChartEmptyState on a chart body). A site that moved over: remove its entry.', + ).toEqual([...REMAINING].sort()); }); it("recognises the idiom it exists to count", () => { @@ -101,7 +111,15 @@ describe("empty states go through EmptyState (ratchet)", () => { path: "components/c.tsx", source: '', }, + { + path: "components/d.tsx", + source: + '

{t("insights.noAnalysisYet")}

', + }, ]), - ).toEqual(["components/a.tsx: admin.aiQuality.empty"]); + ).toEqual([ + "components/a.tsx: admin.aiQuality.empty", + "components/d.tsx: insights.noAnalysisYet", + ]); }); }); diff --git a/src/__tests__/page-header-h1-guard.test.ts b/src/__tests__/page-header-h1-guard.test.ts index 7603d1ea7..b9fd15c05 100644 --- a/src/__tests__/page-header-h1-guard.test.ts +++ b/src/__tests__/page-header-h1-guard.test.ts @@ -10,15 +10,15 @@ * keeps a new `

` literal from appearing outside the places where a * different heading is the point. * - * Matched: an ` = { // Prose pages (the prose H1 tier, §5). "app/about/page.tsx": "prose page", "app/privacy/page.tsx": "prose page", - // Mention an H1 in a comment only. - "app/insights/ecg/page.tsx": "comment", - "components/admin/reminders-section.tsx": "comment", - "components/charts/health-chart.tsx": "comment", - "components/insights/coach-panel/coach-top-bar-trail.tsx": "comment", - "components/insights/ecg-section.tsx": "comment", - "components/measurement-reminders/vorsorge-section.tsx": "comment", - "components/settings/settings-shell.tsx": "comment", }; describe("page titles go through PageHeader", () => { @@ -65,9 +57,8 @@ describe("page titles go through PageHeader", () => { (p.startsWith("components/") || p.startsWith("app/")) && !p.includes("__tests__/"), ); - const withH1 = files.filter((p) => - readFileSync(join(SRC, p), "utf8").includes(" /]/.test(stripComments(source)); + const withH1 = files.filter((p) => hasH1(readFileSync(join(SRC, p), "utf8"))); it("finds the H1 sites at all", () => { expect(withH1.length).toBeGreaterThan(10); @@ -77,6 +68,14 @@ describe("page titles go through PageHeader", () => { expect(withH1.filter((p) => !(p in ALLOWED))).toEqual([]); }); + it("counter-check: an H1 in code counts, one in a comment does not", () => { + expect(hasH1(`return

Title

;`)).toBe(true); + expect(hasH1(`// the page's

lives in PageHeader\nreturn null;`)).toBe( + false, + ); + expect(hasH1(`/* an

above */ return

x

;`)).toBe(false); + }); + it("keeps every allowlist entry live", () => { const set = new Set(withH1); for (const key of Object.keys(ALLOWED)) diff --git a/src/components/insights/insight-status-card.tsx b/src/components/insights/insight-status-card.tsx index 43a1c3af7..2fb013798 100644 --- a/src/components/insights/insight-status-card.tsx +++ b/src/components/insights/insight-status-card.tsx @@ -2,6 +2,7 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; +import { EmptyState } from "@/components/ui/empty-state"; import { TileHeader } from "@/components/insights/tile-header"; import { useTranslations, @@ -228,9 +229,11 @@ export function InsightStatusCard({ -

- {t("insights.noAnalysisYet")} -

+
); diff --git a/src/components/mental-health/instrument-card.tsx b/src/components/mental-health/instrument-card.tsx index df4008a6f..b5360a63f 100644 --- a/src/components/mental-health/instrument-card.tsx +++ b/src/components/mental-health/instrument-card.tsx @@ -30,6 +30,7 @@ * sibling of the detail-open button (never nested inside it) so the two * actions never collide. */ +import { EmptyState } from "@/components/ui/empty-state"; import { Info } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -170,9 +171,12 @@ export function InstrumentCard({

) : ( -
- {t("mentalHealth.noResultYet")} -
+ )} {/* Bottom-pinned single primary action — the med-/Vorsorge card slot.