diff --git a/src/__tests__/unit-preference-display-guard.test.ts b/src/__tests__/unit-preference-display-guard.test.ts index b42229280..a90ff2dbb 100644 --- a/src/__tests__/unit-preference-display-guard.test.ts +++ b/src/__tests__/unit-preference-display-guard.test.ts @@ -56,9 +56,19 @@ const TARGET_SURFACES = [ * the same failure the target block guards against — a converted field * over a canonical save silently rewrites the stored height. */ +/** + * One entry per surface. A surface is a group of files because the + * onboarding step keeps the preference and the adapter where the save + * happens and renders the control from its own inputs file — the three + * conditions below are asserted across the group, not per file, so + * splitting a form does not read as an un-wire. + */ const HEIGHT_SURFACES = [ - join(SRC, "components", "onboarding", "baseline-form.tsx"), - join(SRC, "components", "settings", "account-section", "index.tsx"), + [ + join(SRC, "components", "onboarding", "baseline-form.tsx"), + join(SRC, "components", "onboarding", "baseline-fields.tsx"), + ], + [join(SRC, "components", "settings", "account-section", "index.tsx")], ]; const HEIGHT_CONTROL = join( @@ -153,21 +163,25 @@ describe("unit-preference display guard", () => { // from one form and it falls back to centimetres while its sibling // shows feet and inches. const offenders: string[] = []; - for (const file of HEIGHT_SURFACES) { - const src = readFileSync(file, "utf8"); + for (const files of HEIGHT_SURFACES) { + const surface = files.join(" + "); + const src = files.map((file) => readFileSync(file, "utf8")).join("\n"); if (!src.includes("useUnitDisplay()")) { - offenders.push(`${file}: no preference hook`); + offenders.push(`${surface}: no preference hook`); } if (!src.includes("resolveHeightUnitAdapter")) { - offenders.push(`${file}: no height adapter`); + offenders.push(`${surface}: no height adapter`); } - if (!src.includes("HeightFieldControl")) { - offenders.push(`${file}: hand-rolled height input`); + // Word-bounded: a renamed local wrapper whose name merely starts + // with the control's satisfies a substring check while rendering + // something else entirely. + if (!/\bHeightFieldControl\b/.test(src)) { + offenders.push(`${surface}: hand-rolled height input`); } // The canonical centimetre guardrails must come from the // adapter's inward-rounded bounds, never from a literal. if (/min=\{50\}|max=\{300\}/.test(src)) { - offenders.push(`${file}: hardcoded centimetre guardrail`); + offenders.push(`${surface}: hardcoded centimetre guardrail`); } } expect(offenders).toEqual([]); diff --git a/src/lib/profile/rejected-fields.ts b/src/lib/profile/rejected-fields.ts index d4a2deef0..da0a3e604 100644 --- a/src/lib/profile/rejected-fields.ts +++ b/src/lib/profile/rejected-fields.ts @@ -92,10 +92,11 @@ const REJECTION_FALLBACK_KEY = "settings.profileRejection.other"; * twice. Where a field is refused for several reasons at once the first * one wins — the others are restatements of the same wrong value. * - * `labelKeys` is accepted for symmetry with - * `describeRejectedProfileField` and for the caller that wants to know - * which fields it can actually render; a path the calling screen has no - * input for still gets an entry, so nothing is dropped on the floor. + * No label map is needed: the sentence sits under the input, which + * already carries the label, and the key is the schema path so the + * calling screen can place it without a rename. A path the screen has + * no input for still gets an entry rather than being dropped, so the + * caller can decide what to do with it. */ export function describeRejectedProfileFields( fields: RejectedProfileField[] | undefined,