Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion packages/extension/src/manager/panes/persona-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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) && (
<span style={{ color: c.muted, whiteSpace: "nowrap", flexShrink: 0 }}>{f.label} ·</span>
)}
<AttributeValue registry={registry}
Expand Down
80 changes: 80 additions & 0 deletions packages/extension/tests/persona-pane.render.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -1036,3 +1036,83 @@ test("the pane says the table did not arrive, rather than letting it look like a
assert.match(text, /What you have decided for yourself still stands/);
await ui.unmount();
});

// ── A label that repeats its own value ─────────────────────────────────────
//
// From a live pane: a `company` attribute labelled "Affinidi" holding
// "Affinidi" drew **Affinidi · Affinidi**. A label is a note to self and earns
// its place beside the value; when it *is* the value it earns nothing, and the
// separator makes it read as two facts rather than one said twice.

test("a label that repeats the value is not drawn twice", async () => {
// `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();
});
Loading