From 2db387f65cbd683ef222a9b916906f09cc854bdb Mon Sep 17 00:00:00 2001 From: Glenn Gore Date: Tue, 8 Sep 2026 19:11:29 +0200 Subject: [PATCH] fix(persona): a label that repeats its own value is not drawn twice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From a live pane: a `company` attribute labelled "Affinidi" holding "Affinidi" drew **Affinidi · Affinidi**. A label is a note to self — "work mobile", "the flat" — and it earns its place beside the value; when it *is* the value it earns nothing, and the separator makes one fact read as two. Compared case- and space-insensitively, because "affinidi" beside "Affinidi" is the same stutter with a different shift key. Only a string value is compared: a JSON object rendered beside a label never repeats it, and stringifying one here to find out would be work in aid of a case that cannot arise. Three tests, and the middle one is the reason this is a comparison rather than "hide the label": "work mobile" beside a number is exactly what a label is for. The fixtures carry `sensitivity: "normal"`, which is not incidental — `company` is unregistered and would otherwise be masked, and the first test passed for the wrong reason until they did: there was no value on the card to be repeated. It is also how the pane the report came from was set up. 392 extension tests pass (3 new), 578 core; `tsc -b` and `npm run build` clean. Signed-off-by: Glenn Gore --- .../src/manager/panes/persona-map.tsx | 23 +++++- .../tests/persona-pane.render.test.mts | 80 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/packages/extension/src/manager/panes/persona-map.tsx b/packages/extension/src/manager/panes/persona-map.tsx index 898d450..958248b 100644 --- a/packages/extension/src/manager/panes/persona-map.tsx +++ b/packages/extension/src/manager/panes/persona-map.tsx @@ -121,6 +121,27 @@ function standingWords(tally: ContextTally): string { return parts.join(" · "); } +/** + * Whether the holder's own label is telling the reader anything the value does + * not already say. + * + * A label is a note to self — "work mobile", "the flat" — and it earns its + * place beside the value. When it *is* the value it earns nothing: a `company` + * attribute labelled "Affinidi" holding "Affinidi" drew **Affinidi · Affinidi**, + * which reads as a stutter and, worse, as two facts. + * + * Compared case- and space-insensitively, because "affinidi" beside "Affinidi" + * is the same stutter with a different shift key. Only a string value is + * compared: a JSON object rendered beside a label never repeats it, and + * stringifying one here to find out would be work in aid of a case that cannot + * arise. + */ +function labelSaysSomethingElse(label: string | undefined, value: unknown): boolean { + if (!label) return false; + if (typeof value !== "string") return true; + return label.trim().toLowerCase() !== value.trim().toLowerCase(); +} + function staleWords(reason: string | undefined): string { switch (reason) { case "expired": @@ -641,7 +662,7 @@ export function IdentityMap({ on a card holding nothing the sentence that matters is the one about where the value is. It returns the moment the value does. */} - {f.label && f.value !== undefined && ( + {f.value !== undefined && labelSaysSomethingElse(f.label, f.value) && ( {f.label} · )} { + // `sensitivity: "normal"` because `company` is unregistered and would + // otherwise be masked — which is how the pane the report came from was set + // up, and without it this test would pass on a card showing no value at all. + const doubled = [{ ...attribute("f1", "company", "Affinidi"), label: "Affinidi", sensitivity: "normal" }]; + const a = agent({}); + const ui = await render( + h(IdentityMap, { + parties: PARTIES, + authority: HOLDER, + registry: REGISTRY, + graph: buildGraph(doubled, [], []), + attributes: doubled, + profiles: [], + records: CONTEXTS, + history: [], + onReveal: async () => "never asked", + onChanged: () => {}, + }), + { chrome: { runtime: { sendMessage: a.sendMessage } } }, + ); + const text = ui.text(); + assert.match(text, /Affinidi/, "the value is still there"); + assert.doesNotMatch(text, /Affinidi · Affinidi/); + await ui.unmount(); +}); + +test("a label that says something the value does not is kept", async () => { + // The paired positive, and the reason the check is not simply "hide labels": + // "work mobile" beside a number is the whole point of having one. + const noted = [{ ...attribute("f2", "phone.mobile", "+65 8262 2325"), label: "work mobile" }]; + const a = agent({}); + const ui = await render( + h(IdentityMap, { + parties: PARTIES, + authority: HOLDER, + registry: REGISTRY, + graph: buildGraph(noted, [], []), + attributes: noted, + profiles: [], + records: CONTEXTS, + history: [], + onReveal: async () => "never asked", + onChanged: () => {}, + }), + { chrome: { runtime: { sendMessage: a.sendMessage } } }, + ); + assert.match(ui.text(), /work mobile ·/); + await ui.unmount(); +}); + +test("the same word in a different case is the same stutter", async () => { + const cased = [{ ...attribute("f3", "company", "Affinidi"), label: "affinidi ", sensitivity: "normal" }]; + const a = agent({}); + const ui = await render( + h(IdentityMap, { + parties: PARTIES, + authority: HOLDER, + registry: REGISTRY, + graph: buildGraph(cased, [], []), + attributes: cased, + profiles: [], + records: CONTEXTS, + history: [], + onReveal: async () => "never asked", + onChanged: () => {}, + }), + { chrome: { runtime: { sendMessage: a.sendMessage } } }, + ); + assert.doesNotMatch(ui.text(), /affinidi ·/i); + await ui.unmount(); +});