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(); +});