mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(desktop): npub identity displays for mention, member, and workflow surfaces (#7495)
🤖 ## Summary Every Buzz account is identified by a long public key. Before this change, when someone had no display name, surfaces fell back to inconsistent labels — mostly raw hex fragments like `abcd1234…wxyz`, sometimes a generic role label with no key — so the same person looked different from surface to surface, and nothing looked like an npub address. This PR applies the npub identity foundation from #7488 to the everyday surfaces: a person without a display name now falls back to the same compact npub everywhere — `npub1xxxx…yyyy`, the human-readable spelling of their public key (first 8 + last 4 characters of the full npub) — across messages and mentions, reactions, huddles, member and participant lists, the sidebar and channel activity, search, projects, tray, notifications, and workflow surfaces. - **Mentions and messages**: key-only mention chips render the compact npub. Pasting a copied mention back still re-binds it byte-exactly to the identity it declares, for both the new npub chips and legacy hex-truncated chips copied by older clients — wrong, missing, or tampered key qualification is rejected instead of silently degrading to plain text. - **Reactions and huddles**: huddle reaction events and the huddle roster/participants render the compact npub for unnamed participants; workflow reaction triggers describe authors with the same form. - **Members and sidebar**: channel and community member lists, add-member results and invites, the members sidebar, the channel-activity popover, search, projects (assignees/reviewers/PR panels), the tray menu, and desktop notifications all fall back to the compact npub; titles and aria labels keep the machine-readable full labels. - **Profile labels**: panel/popover display names and owner handles fall back to the compact npub (never raw hex) when there is no name; linked-event (nevent) message metadata shows the npub-shaped author fallback while the event lookup and event IDs are unchanged. - **Workflows**: author-picker secondary labels, step destination keys, and trigger-author references render compact npubs; event and blob IDs keep their existing hex compacts (they are not identities). - **Avatars stay distinct**: fallback avatars for key-only identities derive initials from the key's tail, so prefixed role labels like "Participant npub1…" no longer collapse every unnamed participant onto the same initials; people with names keep their name initials. Preserved exactly: display names and distinct avatars, internal hex keys (storage/API forms unchanged), clipboard identity roundtrips, event/blob ID compaction, private keys (no nsec path is touched), and nevent link handling. Scope: this PR changes what identity labels **display**, not identity controls — profile/settings copy controls, the respond-to allowlist, workflow key fields, and agent dialogs are the sibling slice #7489, and the shared primitives (`canonicalNpub`, `truncateNpub`, the `<PubKey>` gate, strict input parsing) come from the foundation #7488. ### Related issue - Fixes: N/A. Searched existing issues/PRs for duplicates — none found; the related work is the npub identity stack this slice belongs to. - Base/dependency: stacks on #7488 (foundation) — this PR does not stand alone on main. - #7489 is a sibling slice on the same #7488 base (profile/agent/workflow controls), not a dependency: this PR does not require #7489, and #7489 does not require this PR — both only require #7488. ### Testing At exact head `4763cbeae1dd521309755e6d61f657324cb98667` (base: `fix/desktop-npub-identity-d1a` @ `5f3a4a8111998c8aa41ad77cf66992bd1c85343c`; 71 files, +656/−189 — production +277/−136, test support +379/−53): - At this head: targeted `mentions.spec.ts` (1/1), the e2e build, typecheck, and biome — green. - 9 changed/related unit files: 100/100 green; typecheck, e2e build, biome, and px text/truncation checks clean; huddle-roster focused run green; channel-activity e2e 11/11; mutation checks confirm the fallback wiring (removing it collapses shared initials and drops fallback rows). - Known pre-existing local e2e failures, unchanged by this PR and reproduced identically at the upstream merge-base: huddle-transcription voice-menu attribution (25 pass / 1 fail) and the `workflow-local-controls` 438px caret drift. Not claimed green locally. - Update at head `236af9e6137386737e84d3a474d6bc808a704c50` (test-only follow-ups `1143af345` + `236af9e6`): the `workflow-local-controls` races were fixed in the test drivers, and the 438px diff was shown to be a stale Darwin snapshot baseline (name-row enable switch already absent and `message_posted` already MessageSquare at recording commit `9390e11c9`) and refreshed — the focused screenshot test, including keyboard/caret assertions, now passes locally (twice). The full spec was not rerun after the snapshot refresh; the huddle-transcription item above is unchanged. Label/copy text changes are asserted by the e2e specs (`mentions`, `mention-recipients`, `pubkey-display-screenshots`, `huddle-transcription`, `channel-activity-popover`, `workflow-local-controls`) rather than new screenshots; the screenshot spec pins the compact npub text forms. ### Task provenance Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9` Task: buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -344,6 +344,12 @@ test.describe("channel activity hover preview", () => {
|
||||
await expect(
|
||||
popover.getByTestId(`channel-activity-agent-${AGENT_PUBKEY}`),
|
||||
).toContainText("Charlie");
|
||||
// The authored name also supplies the row avatar's initials.
|
||||
await expect(
|
||||
popover
|
||||
.getByTestId(`channel-activity-agent-${AGENT_PUBKEY}`)
|
||||
.getByText("C", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
popover.getByTestId(`channel-activity-agent-${AGENT_PUBKEY}`),
|
||||
).toContainText("Working");
|
||||
@@ -372,6 +378,56 @@ test.describe("channel activity hover preview", () => {
|
||||
await expect(threadPanel).toContainText("direct thread link");
|
||||
});
|
||||
|
||||
test("unnamed working agents keep distinct key-tail initials, not AN", async ({
|
||||
page,
|
||||
}) => {
|
||||
// npubEncode of the fixture keys: "a"×64 → npub1424…rcaj (tail RC),
|
||||
// "b"×64 → npub1hwa…04hu (tail 04). Neither has a seeded profile, so
|
||||
// the rows show the generated "Agent npub1…" fallback label.
|
||||
const unnamedAgents = [
|
||||
{ initials: "RC", label: "Agent npub1424…rcaj", pubkey: "a".repeat(64) },
|
||||
{ initials: "04", label: "Agent npub1hwa…04hu", pubkey: "b".repeat(64) },
|
||||
];
|
||||
|
||||
await page.goto("/");
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
typeof (window as Window & { __BUZZ_E2E_SEED_ACTIVE_TURNS__?: unknown })
|
||||
.__BUZZ_E2E_SEED_ACTIVE_TURNS__ === "function",
|
||||
);
|
||||
for (const { pubkey } of unnamedAgents) {
|
||||
await page.evaluate(
|
||||
({ agentPubkey, channelId }) => {
|
||||
(
|
||||
window as Window & {
|
||||
__BUZZ_E2E_SEED_ACTIVE_TURNS__?: (input: {
|
||||
agentPubkey: string;
|
||||
channelId: string;
|
||||
turnId: string;
|
||||
}) => void;
|
||||
}
|
||||
).__BUZZ_E2E_SEED_ACTIVE_TURNS__?.({
|
||||
agentPubkey,
|
||||
channelId,
|
||||
turnId: "unnamed-agent-initials",
|
||||
});
|
||||
},
|
||||
{ agentPubkey: pubkey, channelId: CHANNEL_GENERAL },
|
||||
);
|
||||
}
|
||||
|
||||
const popover = await openActivityPopover(page);
|
||||
for (const { initials, label, pubkey } of unnamedAgents) {
|
||||
const row = popover.getByTestId(`channel-activity-agent-${pubkey}`);
|
||||
await expect(row).toContainText(label);
|
||||
// The avatar abbreviates its key's visible tail (UserAvatar's fallback
|
||||
// settles after its 200ms delay; expect retries past it), never the
|
||||
// "Agent" word initials of the prefixed label.
|
||||
await expect(row.getByText(initials, { exact: true })).toBeVisible();
|
||||
await expect(row.getByText("AN", { exact: true })).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
|
||||
test("removes the dot and preview after the final activity is read", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -4176,8 +4176,26 @@ test("members sidebar virtualizes large channel rosters", async ({ page }) => {
|
||||
await expect(memberRows.first()).toBeVisible();
|
||||
expect(await memberRows.count()).toBeLessThan(50);
|
||||
|
||||
// Generated members have no display name, so the roster sorts them by
|
||||
// their npub fallback label: these sequential pubkeys share a `npub1qqq…`
|
||||
// prefix and order by the checksum tail, not their numeric value. Resolve
|
||||
// a generated member from the rows the initial window actually rendered
|
||||
// instead of assuming `pubkeys[0]` sorts into that window.
|
||||
const generatedPubkeySet = new Set(pubkeys);
|
||||
const renderedPubkeys = await memberRows.evaluateAll((rows) =>
|
||||
rows.map(
|
||||
(row) =>
|
||||
(row as HTMLElement).dataset.testid?.slice("sidebar-member-".length) ??
|
||||
"",
|
||||
),
|
||||
);
|
||||
const firstRenderedGeneratedPubkey = renderedPubkeys.find((pubkey) =>
|
||||
generatedPubkeySet.has(pubkey),
|
||||
);
|
||||
expect(firstRenderedGeneratedPubkey).toBeTruthy();
|
||||
|
||||
const firstGeneratedRow = memberList.getByTestId(
|
||||
`sidebar-member-${pubkeys[0]}`,
|
||||
`sidebar-member-${firstRenderedGeneratedPubkey}`,
|
||||
);
|
||||
await expect
|
||||
.poll(() =>
|
||||
@@ -4214,8 +4232,24 @@ test("members sidebar virtualizes large channel rosters", async ({ page }) => {
|
||||
element.scrollTop = element.scrollHeight;
|
||||
element.dispatchEvent(new Event("scroll"));
|
||||
});
|
||||
// Fully scrolling must render the roster's true tail, and the tail
|
||||
// endpoint must be known independently of whatever the virtual window
|
||||
// happens to render. The sidebar's own roster accounting — the
|
||||
// "Members · N" header — must read exactly the fixture-known total
|
||||
// ("random" seeds alice, the mock identity, and bob; this test adds the
|
||||
// 500 generated pubkeys on top), so fixture or classification drift
|
||||
// fails loudly here instead of silently weakening the tail check.
|
||||
const rosterCount = 3 + pubkeys.length;
|
||||
await expect(memberList.getByText(/^Members · \d+$/)).toHaveText(
|
||||
`Members · ${rosterCount}`,
|
||||
);
|
||||
// VirtualizedList stamps each rendered row with its item index, so the
|
||||
// final item's row is a fixed target that no window sample can pick in
|
||||
// its place: a virtualizer clamped mid-roster never renders it.
|
||||
await expect(
|
||||
memberList.getByTestId(`sidebar-member-${pubkeys.at(-1)}`),
|
||||
memberList.locator(
|
||||
`[data-index="${rosterCount - 1}"] > [data-testid^="sidebar-member-"]`,
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
KIND_HUDDLE_ENDED,
|
||||
KIND_HUDDLE_STARTED,
|
||||
} from "../../src/shared/constants/kinds";
|
||||
import { truncateNpub } from "../../src/shared/lib/pubkey";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
||||
|
||||
@@ -11,6 +12,15 @@ const HUDDLE_CHANNEL_ID = "11111111-1111-4111-8111-111111111111";
|
||||
const HUDDLE_PARENT_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
|
||||
const HUDDLE_THREAD_ROOT_ID = "mock-general-welcome";
|
||||
|
||||
/**
|
||||
* Initials a key-fallback avatar shows: the two characters just before the
|
||||
* compact npub label's visible tail — the fragment `getInitials` reads from
|
||||
* a key label, whose unit suite pins the derivation itself.
|
||||
*/
|
||||
function keyTailInitials(pubkey: string) {
|
||||
return truncateNpub(pubkey).slice(-4, -2).toUpperCase();
|
||||
}
|
||||
|
||||
async function waitForMockLiveSubscription(
|
||||
page: import("@playwright/test").Page,
|
||||
channelName: string,
|
||||
@@ -1075,10 +1085,19 @@ test("returns to the parent channel when leaving a huddle channel in view", asyn
|
||||
test("keeps the huddle avatar strip compact and exposes the full roster", async ({
|
||||
page,
|
||||
}) => {
|
||||
const members = Array.from({ length: 11 }, (_, index) => ({
|
||||
// One named member plus ten key-only members (hex 01..0a + "a"×62), with
|
||||
// the last as the huddle bot. The generated keys' npub tails are what
|
||||
// key-fallback avatars abbreviate: 01… → npub1qx4…x2tc → X2,
|
||||
// 02… → npub1q24…levs → LE, 0a… → npub1p24…sqjt → SQ.
|
||||
const namedMember = "77".repeat(32);
|
||||
const unnamedMembers = Array.from({ length: 10 }, (_, index) => ({
|
||||
pubkey: `${(index + 1).toString(16).padStart(2, "0")}${"a".repeat(62)}`,
|
||||
role: index === 10 ? ("bot" as const) : ("member" as const),
|
||||
role: index === 9 ? ("bot" as const) : ("member" as const),
|
||||
}));
|
||||
const members = [
|
||||
{ pubkey: namedMember, role: "member" as const },
|
||||
...unnamedMembers,
|
||||
];
|
||||
|
||||
await installMockBridge(page, {
|
||||
huddle: {
|
||||
@@ -1086,6 +1105,7 @@ test("keeps the huddle avatar strip compact and exposes the full roster", async
|
||||
ephemeralChannelId: HUDDLE_CHANNEL_ID,
|
||||
members,
|
||||
},
|
||||
searchProfiles: [{ pubkey: namedMember, displayName: "Ada Lovelace" }],
|
||||
});
|
||||
await page.goto("/");
|
||||
|
||||
@@ -1100,13 +1120,44 @@ test("keeps the huddle avatar strip compact and exposes the full roster", async
|
||||
await expect(participantTrigger).toContainText("+2");
|
||||
await expect(page.getByTestId("profile-huddle-control")).toHaveCount(0);
|
||||
|
||||
// Strip avatars stay distinct: the named member keeps name initials
|
||||
// (AL), and a key-only participant shows its npub's tail (X2), never the
|
||||
// word initials of the "Participant npub1…" label (PN).
|
||||
const stripAvatars = participantStrip.getByTestId(
|
||||
"huddle-participant-avatar",
|
||||
);
|
||||
await expect(stripAvatars.first()).toHaveText("AL");
|
||||
await expect(stripAvatars.nth(1)).toHaveText(
|
||||
keyTailInitials(unnamedMembers[0].pubkey),
|
||||
);
|
||||
|
||||
await participantTrigger.click();
|
||||
const roster = page
|
||||
.getByRole("dialog")
|
||||
.filter({ has: page.getByRole("heading", { name: "Participants" }) });
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Participants" }),
|
||||
).toBeVisible();
|
||||
const rosterRows = roster.getByRole("listitem");
|
||||
// Visible labels keep their generated forms alongside the avatar initials.
|
||||
await expect(rosterRows.filter({ hasText: "Ada Lovelace" })).toHaveCount(1);
|
||||
await expect(
|
||||
rosterRows.filter({
|
||||
hasText: `Participant ${truncateNpub(unnamedMembers[0].pubkey)}`,
|
||||
}),
|
||||
).toHaveCount(1);
|
||||
const agentRow = rosterRows.filter({
|
||||
hasText: `Agent ${truncateNpub(unnamedMembers[9].pubkey)}`,
|
||||
});
|
||||
await expect(agentRow).toHaveCount(1);
|
||||
await expect(
|
||||
page.getByRole("button", { name: /Remove Agent .* from huddle/ }),
|
||||
).toHaveCount(1);
|
||||
// The roster's agent avatar abbreviates its key tail (SQ), never the
|
||||
// "Agent" word initials of the prefixed label (AN).
|
||||
await expect(agentRow.getByTestId("huddle-participant-avatar")).toHaveText(
|
||||
keyTailInitials(unnamedMembers[9].pubkey),
|
||||
);
|
||||
});
|
||||
|
||||
test("removes an agent from its menu without showing an extra participant control", async ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { truncatePubkey } from "../../src/shared/lib/pubkey";
|
||||
import { truncateNpub } from "../../src/shared/lib/pubkey";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
||||
|
||||
@@ -566,7 +566,7 @@ for (const { kind, scale } of [
|
||||
.last();
|
||||
await assertFits(markdown, "sent");
|
||||
const qualifiedChip = row.locator("[data-mention]").last();
|
||||
await expect(qualifiedChip).toHaveText(`Scout (${truncatePubkey(SECOND)})`);
|
||||
await expect(qualifiedChip).toHaveText(`Scout (${truncateNpub(SECOND)})`);
|
||||
await expect(qualifiedChip).toHaveAttribute(
|
||||
"data-mention-label",
|
||||
`Scout (${SECOND})`,
|
||||
@@ -873,7 +873,7 @@ for (const mismatchedKey of [false, true]) {
|
||||
.getByTestId("message-row")
|
||||
.filter({ hasText: "qualified clipboard roundtrip" })
|
||||
.locator(`[data-mention-pubkey="${SECOND}"]`);
|
||||
await expect(chip).toHaveText(`Scout (${truncatePubkey(SECOND)})`);
|
||||
await expect(chip).toHaveText(`Scout (${truncateNpub(SECOND)})`);
|
||||
const flavors = await chip.evaluate((element) => {
|
||||
const range = document.createRange();
|
||||
range.selectNode(element);
|
||||
@@ -956,7 +956,7 @@ for (const partial of [false, true]) {
|
||||
.filter({ hasText: "compact collision" });
|
||||
for (const key of keys) {
|
||||
const chip = row.locator(`[data-mention-pubkey="${key}"]`);
|
||||
await expect(chip).toHaveText(`Scout (${truncatePubkey(key)})`);
|
||||
await expect(chip).toHaveText(`Scout (${truncateNpub(key)})`);
|
||||
await expect(chip).toHaveAttribute("title", `Scout (${key})`);
|
||||
const flavors = await chip.evaluate((element, partial) => {
|
||||
const range = document.createRange();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { npubEncode } from "nostr-tools/nip19";
|
||||
|
||||
import { truncateNpub } from "../../src/shared/lib/pubkey";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
|
||||
import {
|
||||
@@ -4535,9 +4535,8 @@ test("clicking author name opens user profile panel", async ({ page }) => {
|
||||
// Click now opens the full profile panel instead of the popover
|
||||
const panel = page.getByTestId("user-profile-panel");
|
||||
await expect(panel).toBeVisible();
|
||||
// The panel's public key row renders through the shared <PubKey> widget,
|
||||
// which displays the canonical npub form — assert the npub prefix.
|
||||
await expect(panel).toContainText(npubEncode(MOCK_VIEWER_PUBKEY).slice(0, 8));
|
||||
await expect(panel).toContainText(truncateNpub(MOCK_VIEWER_PUBKEY));
|
||||
await expect(panel).not.toContainText("deadbeefdeadbeef");
|
||||
});
|
||||
|
||||
test("hovering avatar opens popover, clicking opens profile panel", async ({
|
||||
@@ -4760,7 +4759,7 @@ test("agent profile popover falls back to the owner's pubkey", async ({
|
||||
profilePopover.getByTestId(
|
||||
`user-profile-popover-owner-${OWNED_AGENT_PROFILE_PUBKEY}`,
|
||||
),
|
||||
).toHaveText("managed by 11111111…1111");
|
||||
).toHaveText(`managed by ${truncateNpub(CASEY_PROFILE_PUBKEY)}`);
|
||||
});
|
||||
|
||||
test("human profile popover does not show an owner", async ({ page }) => {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { npubEncode } from "nostr-tools/nip19";
|
||||
|
||||
import { truncateNpub } from "../../src/shared/lib/pubkey";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import {
|
||||
installMockBridge,
|
||||
openNewMessagePage,
|
||||
TEST_IDENTITIES,
|
||||
} from "../helpers/bridge";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
|
||||
const SHOTS = "test-results/pubkey-display";
|
||||
|
||||
@@ -121,7 +122,7 @@ test("new-DM agent name swaps to its public key on name hover", async ({
|
||||
)
|
||||
.toBe(true);
|
||||
await expect(settledAgentNpub).not.toHaveCSS("opacity", "0");
|
||||
await expect(settledAgentNpub).toHaveText("cafef00d…f00d");
|
||||
await expect(settledAgentNpub).toHaveText(truncateNpub(AGENT_PUBKEY));
|
||||
await expect(
|
||||
settledAgentName.getByText("Pinky", { exact: true }),
|
||||
).not.toHaveCSS("opacity", "1");
|
||||
@@ -159,7 +160,7 @@ test("selected new-DM recipient can be verified again through search", async ({
|
||||
await charlieName.hover();
|
||||
await expect(charlieNpub).toHaveCSS("opacity", "1");
|
||||
await expect(charlieNpub).toHaveText(
|
||||
`${TEST_IDENTITIES.charlie.pubkey.slice(0, 8)}…${TEST_IDENTITIES.charlie.pubkey.slice(-4)}`,
|
||||
truncateNpub(TEST_IDENTITIES.charlie.pubkey),
|
||||
);
|
||||
await page.mouse.move(1_100, 500);
|
||||
await expect(charlieNpub).toHaveCSS("opacity", "0");
|
||||
@@ -262,7 +263,7 @@ test("selected new-DM recipient can be verified again through search", async ({
|
||||
await charlieName.hover();
|
||||
await expect(charlieNpub).toHaveCSS("opacity", "1");
|
||||
await expect(charlieNpub).toHaveText(
|
||||
`${TEST_IDENTITIES.charlie.pubkey.slice(0, 8)}…${TEST_IDENTITIES.charlie.pubkey.slice(-4)}`,
|
||||
truncateNpub(TEST_IDENTITIES.charlie.pubkey),
|
||||
);
|
||||
await expect(charlieName.getByText("charlie", { exact: true })).toHaveCSS(
|
||||
"opacity",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
||||
|
||||
import { truncateNpub } from "../../src/shared/lib/pubkey";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
|
||||
@@ -63,7 +64,9 @@ async function addMessageStep(
|
||||
) {
|
||||
await dialog.getByRole("button", { name: "Add step", exact: true }).click();
|
||||
await page.getByRole("menuitem", { name: "Send Message" }).click();
|
||||
await dialog.getByLabel("Message text").fill("Workflow notification");
|
||||
await dialog
|
||||
.locator('textarea[id^="wf-step-"][id$="-text"]')
|
||||
.fill("Workflow notification");
|
||||
}
|
||||
|
||||
async function createEnabled(
|
||||
@@ -152,7 +155,7 @@ test("inserts template variables with keyboard control and restores the caret",
|
||||
await dialog.getByRole("button", { name: "Add step", exact: true }).click();
|
||||
await page.getByRole("menuitem", { name: "Send Message" }).click();
|
||||
|
||||
const textarea = dialog.getByLabel("Message text");
|
||||
const textarea = dialog.locator('textarea[id^="wf-step-"][id$="-text"]');
|
||||
const listbox = page.getByRole("listbox");
|
||||
await textarea.fill("Hello {{trig");
|
||||
await expect(listbox).toBeVisible();
|
||||
@@ -286,6 +289,7 @@ test("round-trips and reopens structured message-text conditions", async ({
|
||||
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
await openTriggerInspector(dialog);
|
||||
await waitForAnimations(page);
|
||||
const matchControls = dialog.getByRole("group", { name: "Match" });
|
||||
const operatorButtons = matchControls.getByRole("button");
|
||||
const firstOperatorBox = await operatorButtons.nth(0).boundingBox();
|
||||
@@ -640,11 +644,11 @@ test("round-trips manual author and reaction message IDs through save and reopen
|
||||
});
|
||||
await authorSearch.fill(author);
|
||||
await expect(
|
||||
dialog.getByRole("option", { name: new RegExp(author.slice(0, 8)) }),
|
||||
dialog.getByRole("option", { name: truncateNpub(author) }),
|
||||
).toBeVisible();
|
||||
await authorSearch.press("Enter");
|
||||
await expect(
|
||||
dialog.getByRole("option", { name: new RegExp(author.slice(0, 8)) }),
|
||||
dialog.getByRole("option", { name: truncateNpub(author) }),
|
||||
).toHaveAttribute("aria-selected", "true");
|
||||
await expect(dialog.getByRole("button", { name: "Create" })).toBeEnabled();
|
||||
await dialog.getByRole("tab", { name: "YAML" }).click();
|
||||
@@ -663,7 +667,7 @@ test("round-trips manual author and reaction message IDs through save and reopen
|
||||
});
|
||||
await correctedAuthorSearch.fill(author);
|
||||
await expect(
|
||||
dialog.getByRole("option", { name: new RegExp(author.slice(0, 8)) }),
|
||||
dialog.getByRole("option", { name: truncateNpub(author) }),
|
||||
).toBeVisible();
|
||||
await correctedAuthorSearch.press("Enter");
|
||||
await dialog
|
||||
@@ -697,7 +701,7 @@ test("round-trips manual author and reaction message IDs through save and reopen
|
||||
await openTriggerInspector(reopened);
|
||||
await reopened.getByText("Author", { exact: true }).locator("..").click();
|
||||
await expect(
|
||||
reopened.getByRole("option", { name: new RegExp(author.slice(0, 8)) }),
|
||||
reopened.getByRole("option", { name: truncateNpub(author) }),
|
||||
).toHaveAttribute("aria-selected", "true");
|
||||
await reopened.getByText("Message", { exact: true }).locator("..").click();
|
||||
await expect(
|
||||
@@ -738,7 +742,7 @@ test("toggles selected author and message filters while preserving sibling condi
|
||||
.locator("..");
|
||||
await authorField.getByText("Author", { exact: true }).locator("..").click();
|
||||
const authorOption = dialog.getByRole("option", {
|
||||
name: new RegExp(author.slice(0, 8)),
|
||||
name: truncateNpub(author),
|
||||
});
|
||||
await expect(authorOption).toHaveAttribute("aria-selected", "true");
|
||||
await expect(
|
||||
@@ -751,7 +755,7 @@ test("toggles selected author and message filters while preserving sibling condi
|
||||
});
|
||||
await authorSearch.fill(replacementAuthor);
|
||||
const replacementAuthorOption = dialog.getByRole("option", {
|
||||
name: new RegExp(replacementAuthor.slice(0, 8)),
|
||||
name: truncateNpub(replacementAuthor),
|
||||
});
|
||||
await expect(replacementAuthorOption).toBeVisible();
|
||||
await authorSearch.press("Enter");
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
Reference in New Issue
Block a user