mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
test(share): render the clinician view in all six locales
Most labels on this page are resolved from a runtime value, not written as a literal: an illness type, a lifecycle, a visit kind, a vaccine slug, a blood-type constant, a cycle phase, a GLP-1 side-effect tag. The call-site guard cannot see any of them, and the enum-derived guard can only cover a space whose members it can enumerate from a source. A key that does not exist therefore renders as its own dot notation, in front of a doctor, with every other guard green — which is exactly how `encounters.kind.ROUTINE` reached three surfaces. One fixture with every section populated, six renders, one assertion: nothing in the reader-visible text may look like a dotted key path. Break-proof: put the interpolated visit-kind key back and all six go red naming it.
This commit is contained in:
1 parent
a7452c7a6c
commit
96bcf843fe
1 file changed
+253
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* The clinician view renders in all six shipped locales with no raw i18n key
|
||||
* reaching the reader.
|
||||
*
|
||||
* Most of this page's labels are resolved from a runtime value rather than
|
||||
* written as a literal: an illness type, a lifecycle, a visit kind, a vaccine
|
||||
* slug, a blood-type constant, a cycle phase, a GLP-1 side-effect tag. None of
|
||||
* those are visible to `i18n-call-site-coverage`, which only reads literal
|
||||
* `t("ns.key")` calls, and the enum-derived guard can only cover a key space
|
||||
* whose members it can enumerate from a source. So a key that does not exist
|
||||
* renders as its own dot notation — `encounters.kind.ROUTINE`, verbatim, on a
|
||||
* page a doctor is reading — and every other guard stays green.
|
||||
*
|
||||
* This one renders the whole surface, strips the markup, and fails on anything
|
||||
* left in the text that looks like a dotted key path. One fixture with every
|
||||
* section populated, six locales, one assertion.
|
||||
*
|
||||
* Mutation check: replace any `t(resolver(x))` call in the section files with
|
||||
* `t(\`ns.${x}\`)` over an enum whose members are not the bundle's leaf names,
|
||||
* and all six cases go red naming the leaked key.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { ClinicianView } from "../clinician-view";
|
||||
import { getServerTranslator } from "@/lib/i18n/server-translator";
|
||||
import { computeGlucoseClinicalMetrics } from "@/lib/analytics/glucose-metrics";
|
||||
import { ALL_LEAF_IDS } from "@/lib/report-selection/catalogue";
|
||||
import { selectionFromLeaves } from "@/lib/report-selection/selection";
|
||||
import { locales } from "@/lib/i18n/config";
|
||||
import type { DoctorReportData } from "@/lib/doctor-report-data";
|
||||
|
||||
/**
|
||||
* One record with every section carrying something, so a single render walks
|
||||
* every branch that resolves a label from a value.
|
||||
*/
|
||||
const FULL_RECORD = {
|
||||
period: {
|
||||
days: 30,
|
||||
since: "2026-01-01T00:00:00.000Z",
|
||||
start: "2026-01-01T00:00:00.000Z",
|
||||
end: "2026-01-31T00:00:00.000Z",
|
||||
},
|
||||
patient: {
|
||||
username: "shared-account",
|
||||
dateOfBirth: "1980-01-01T00:00:00.000Z",
|
||||
gender: "FEMALE",
|
||||
heightCm: 170,
|
||||
fullName: "A. Patient",
|
||||
},
|
||||
practiceName: null,
|
||||
measurements: {},
|
||||
stats: { WEIGHT: { avg: 80, min: 78, max: 82, count: 3, latest: 79 } },
|
||||
glucoseStats: {
|
||||
FASTING: { avg: 96, min: 88, max: 104, count: 5, latest: 94 },
|
||||
},
|
||||
glucoseRanges: {},
|
||||
glucoseClinical: computeGlucoseClinicalMetrics([], {
|
||||
now: new Date("2026-01-31T00:00:00.000Z"),
|
||||
}),
|
||||
glucoseUnit: "mg/dL",
|
||||
bmi: 24.5,
|
||||
compliance: { Metformin: { total: 60, taken: 57, skipped: 1, missed: 2 } },
|
||||
medications: [{ name: "Ramipril", dose: "5 mg", schedules: [] }],
|
||||
medicationAdministrations: [
|
||||
{
|
||||
medicationName: "Ramipril",
|
||||
effectiveAt: "2026-01-30T07:10:00.000Z",
|
||||
status: "completed",
|
||||
doseText: "5 mg",
|
||||
dose: null,
|
||||
injectionSite: null,
|
||||
atcCode: null,
|
||||
rxNormCode: null,
|
||||
deliveryForm: "ORAL",
|
||||
},
|
||||
],
|
||||
glp1: {
|
||||
medications: [
|
||||
{
|
||||
name: "Semaglutide",
|
||||
currentDose: {
|
||||
value: 1,
|
||||
unit: "mg",
|
||||
since: "2026-01-05T00:00:00.000Z",
|
||||
},
|
||||
doseHistory: [
|
||||
{
|
||||
value: 0.5,
|
||||
unit: "mg",
|
||||
effectiveFrom: "2025-12-01T00:00:00.000Z",
|
||||
note: null,
|
||||
},
|
||||
],
|
||||
lastInjection: { date: "2026-01-28T00:00:00.000Z", site: "ABDOMEN" },
|
||||
compliance: { taken: 4, total: 4 },
|
||||
},
|
||||
],
|
||||
weightDeltaKg: -3,
|
||||
weightStartKg: 83,
|
||||
weightEndKg: 80,
|
||||
sideEffects: [{ tag: "nausea", count: 2 }],
|
||||
},
|
||||
mood: {
|
||||
avg: 3.4,
|
||||
min: 2,
|
||||
max: 5,
|
||||
count: 22,
|
||||
distribution: { 1: 0, 2: 3, 3: 9, 4: 7, 5: 3 },
|
||||
},
|
||||
cycle: {
|
||||
lastPeriodStart: "2026-01-09",
|
||||
recentCycles: [
|
||||
{ startDate: "2026-01-09", lengthDays: 29, periodLengthDays: 5 },
|
||||
],
|
||||
observedCycleCount: 1,
|
||||
averageCycleLengthDays: 29,
|
||||
cycleLengthVariabilityDays: 1.5,
|
||||
averagePeriodLengthDays: 5,
|
||||
currentPhase: "LUTEAL",
|
||||
},
|
||||
labResults: [
|
||||
{
|
||||
panel: "Blood count",
|
||||
analyte: "Ferritin",
|
||||
value: 42,
|
||||
valueText: null,
|
||||
unit: "ng/mL",
|
||||
referenceLow: 30,
|
||||
referenceHigh: 400,
|
||||
catalogReferenceLow: 30,
|
||||
catalogReferenceHigh: 400,
|
||||
sourceReferenceText: null,
|
||||
referenceOrigin: "catalog",
|
||||
referenceDivergesFromCatalog: false,
|
||||
takenAt: "2026-01-20T09:00:00.000Z",
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
illnessEpisodes: [
|
||||
{
|
||||
label: "Sinusitis",
|
||||
type: "INFECTION",
|
||||
lifecycle: "ACUTE",
|
||||
onsetAt: "2026-01-08T00:00:00.000Z",
|
||||
resolvedAt: null,
|
||||
},
|
||||
],
|
||||
visits: [
|
||||
{
|
||||
occurredAt: "2026-01-14T08:30:00.000Z",
|
||||
kind: "SPECIALIST",
|
||||
status: "DONE",
|
||||
practitionerName: "Cardiology outpatients",
|
||||
practitionerSpecialty: "Cardiology",
|
||||
reason: "Palpitations",
|
||||
outcome: "Follow up in three months",
|
||||
conditionLabels: ["Sinusitis"],
|
||||
},
|
||||
],
|
||||
immunizations: [
|
||||
{
|
||||
occurredAt: "2025-11-03T00:00:00.000Z",
|
||||
antigenSlug: "tetanus",
|
||||
vaccineName: null,
|
||||
lotNumber: "LOT-7781",
|
||||
site: null,
|
||||
practitionerName: null,
|
||||
series: [{ antigen: "tetanus", position: 3, total: 4, booster: false }],
|
||||
},
|
||||
],
|
||||
allergies: [
|
||||
{
|
||||
substance: "Penicillin",
|
||||
category: "MEDICATION",
|
||||
type: "ALLERGY",
|
||||
severity: "SEVERE",
|
||||
status: "ACTIVE",
|
||||
reaction: "Hives",
|
||||
reactionUnreadable: false,
|
||||
},
|
||||
],
|
||||
familyHistory: [
|
||||
{ relationship: "MOTHER", condition: "Type 2 diabetes", ageAtOnset: 54 },
|
||||
],
|
||||
anamnesis: {
|
||||
conditions: "Hypothyroidism",
|
||||
conditionsUnreadable: false,
|
||||
smokingStatus: "FORMER",
|
||||
alcoholPattern: "OCCASIONAL",
|
||||
shiftSchedule: "ROTATING",
|
||||
unreadableFacts: [],
|
||||
},
|
||||
emergency: {
|
||||
bloodType: "O_NEG",
|
||||
organDonor: "YES",
|
||||
advanceDirective: "EXISTS",
|
||||
contacts: "Next of kin, 555 0100",
|
||||
contactsUnreadable: false,
|
||||
implants: "Pacemaker",
|
||||
implantsUnreadable: false,
|
||||
note: "Carries an emergency card",
|
||||
noteUnreadable: false,
|
||||
},
|
||||
wellnessScores: [
|
||||
{
|
||||
type: "RECOVERY_SCORE",
|
||||
latest: 72,
|
||||
avg: 68,
|
||||
min: 50,
|
||||
max: 90,
|
||||
count: 20,
|
||||
latestAt: "2026-01-30T00:00:00.000Z",
|
||||
},
|
||||
],
|
||||
} as unknown as DoctorReportData;
|
||||
|
||||
/**
|
||||
* `a.b.c` and deeper, lowercase-initial: the shape an unresolved key falls
|
||||
* through as. Two segments would catch ordinary prose ("e.g. this"), so the
|
||||
* floor is three — every namespace on this page is at least that deep.
|
||||
*/
|
||||
const DOTTED_KEY = /\b[a-z][A-Za-z0-9]*(\.[A-Za-z0-9_]+){2,}\b/g;
|
||||
|
||||
describe("<ClinicianView> resolves every label in every locale", () => {
|
||||
it("covers all six shipped locales", () => {
|
||||
// A floor: an empty locale list would make the loop below assert nothing.
|
||||
expect(locales.length).toBe(6);
|
||||
});
|
||||
|
||||
for (const locale of locales) {
|
||||
it(`leaks no raw i18n key in ${locale}`, () => {
|
||||
const { t } = getServerTranslator(locale);
|
||||
const html = renderToStaticMarkup(
|
||||
ClinicianView({
|
||||
t: (key, vars) => t(key, vars),
|
||||
label: "Clinic",
|
||||
expiresAt: "2026-03-01T00:00:00.000Z",
|
||||
report: FULL_RECORD,
|
||||
selection: selectionFromLeaves(ALL_LEAF_IDS),
|
||||
locale,
|
||||
}),
|
||||
);
|
||||
// Strip the tags, so the `data-leaf` enum lists and the class names go
|
||||
// with them and only what a reader sees is left.
|
||||
const text = html.replace(/<[^>]*>/g, " ");
|
||||
expect(
|
||||
text.match(DOTTED_KEY) ?? [],
|
||||
`these keys rendered as their own dot notation in ${locale}`,
|
||||
).toEqual([]);
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user