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"; const SHOTS = "test-results/pubkey-display"; const AGENT_PUBKEY = "cafef00d".repeat(8); // Screenshot evidence for the pubkey-display work: the canonical profile // surfaces, plus the new-DM recipient identity-hover states retained by it. test("profile panel Public key row reveals its copy action on hover", async ({ page, }) => { await installMockBridge(page); await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); const messageRow = page.getByTestId("message-row").first(); await expect(messageRow).toBeVisible(); await messageRow.locator("button").first().click(); await expect(page.getByTestId("user-profile-panel")).toBeVisible(); const pubkeyRow = page.getByTestId("user-profile-public-key"); const copyIndicator = page.getByTestId("user-profile-public-key-copy-status"); await expect(page.getByTestId("user-profile-copy-pubkey")).toBeVisible(); await expect(copyIndicator).toHaveCSS("opacity", "0"); await pubkeyRow.hover(); await expect(copyIndicator).toHaveCSS("opacity", "1"); await waitForAnimations(page); await page.screenshot({ path: `${SHOTS}/profile-panel-pubkey-hover-copy.png`, }); }); test("new-DM agent name swaps to its public key on name hover", async ({ page, }) => { // Agent rows only surface when the agent is mentionable, so seed a managed // agent alongside its search profile. await installMockBridge(page, { managedAgents: [ { name: "Pinky", pubkey: AGENT_PUBKEY, status: "running", }, ], searchProfiles: [ { displayName: "Pinky", isAgent: true, ownerPubkey: "deadbeef".repeat(8), pubkey: AGENT_PUBKEY, }, ], }); await page.goto("/"); await openNewMessagePage(page); await expect(page.getByTestId("new-message-page")).toBeVisible(); const agentResult = page.getByTestId(`new-dm-result-${AGENT_PUBKEY}`); await expect(agentResult).toBeVisible(); await expect(page.getByTestId("new-dm-loading")).toBeHidden(); await expect .poll(async () => { const marker = crypto.randomUUID(); await agentResult.evaluate((element, value) => { element.dataset.e2eSettleMarker = value; }, marker); await page.waitForTimeout(250); return agentResult .evaluate( (element, value) => element.dataset.e2eSettleMarker === value, marker, ) .catch(() => false); }) .toBe(true); const agentName = agentResult.getByTestId(`new-dm-name-${AGENT_PUBKEY}`); const agentNpub = agentResult.getByTestId(`new-dm-npub-${AGENT_PUBKEY}`); await expect(agentResult).toContainText("managed by you"); await expect(agentName).toContainText("Pinky"); await expect(agentNpub).toHaveCSS("opacity", "0"); const agentNameBox = await agentName.boundingBox(); const agentResultBox = await agentResult.boundingBox(); expect(agentNameBox).not.toBeNull(); expect(agentResultBox).not.toBeNull(); if (!agentNameBox || !agentResultBox) return; expect(agentNameBox.width).toBeLessThan(agentResultBox.width / 2); await page.mouse.move( agentResultBox.x + agentResultBox.width - 12, agentNameBox.y + agentNameBox.height / 2, ); await expect(agentNpub).toHaveCSS("opacity", "0"); // Acquire fresh locators after the directory queries settle: the result row // can be replaced while the initial loading skeleton is transitioning out. const settledAgentResult = page.getByTestId(`new-dm-result-${AGENT_PUBKEY}`); const settledAgentName = settledAgentResult.getByTestId( `new-dm-name-${AGENT_PUBKEY}`, ); const settledAgentNpub = settledAgentResult.getByTestId( `new-dm-npub-${AGENT_PUBKEY}`, ); await settledAgentName.hover(); await expect .poll(async () => settledAgentName.evaluate((element) => element.matches(":hover")), ) .toBe(true); await expect(settledAgentNpub).not.toHaveCSS("opacity", "0"); await expect(settledAgentNpub).toHaveText(truncateNpub(AGENT_PUBKEY)); await expect( settledAgentName.getByText("Pinky", { exact: true }), ).not.toHaveCSS("opacity", "1"); await expect(settledAgentResult).toContainText("managed by you"); await waitForAnimations(page); await page.getByTestId("new-message-page").screenshot({ path: `${SHOTS}/new-dm-agent-name-hover.png`, }); }); test("selected new-DM recipient can be verified again through search", async ({ page, }) => { await installMockBridge(page); await page.goto("/"); await openNewMessagePage(page); await expect(page.getByTestId("new-message-page")).toBeVisible(); const search = page.getByTestId("new-dm-search"); const charlieResult = page.getByTestId( `new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`, ); const charlieName = page.getByTestId( `new-dm-name-${TEST_IDENTITIES.charlie.pubkey}`, ); const charlieNpub = page.getByTestId( `new-dm-npub-${TEST_IDENTITIES.charlie.pubkey}`, ); // The same name-to-key swap is available in the unfiltered directory, // before the user has typed anything. await expect(charlieResult).toBeVisible(); await expect(charlieNpub).toHaveCSS("opacity", "0"); await charlieName.hover(); await expect(charlieNpub).toHaveCSS("opacity", "1"); await expect(charlieNpub).toHaveText( truncateNpub(TEST_IDENTITIES.charlie.pubkey), ); await page.mouse.move(1_100, 500); await expect(charlieNpub).toHaveCSS("opacity", "0"); await search.fill("charlie"); await expect(charlieResult).toBeVisible(); await page.keyboard.press("Enter"); const charlieChip = page.getByTestId( `new-dm-selected-${TEST_IDENTITIES.charlie.pubkey}`, ); const charlieNameTrigger = page.getByTestId( `new-dm-recipient-name-${TEST_IDENTITIES.charlie.pubkey}`, ); const charlieKeyPopover = page.getByTestId( `new-dm-selected-key-popover-${TEST_IDENTITIES.charlie.pubkey}`, ); const charliePubkey = page.getByTestId( `new-dm-selected-pubkey-${TEST_IDENTITIES.charlie.pubkey}`, ); await expect(charlieChip).toBeVisible(); await expect(page.getByTestId("new-message-recipient-popover")).toBeVisible(); await expect(search).toHaveValue(""); await expect(charlieResult).toHaveCount(0); await expect(charlieKeyPopover).toHaveCount(0); await charlieChip.hover(); await expect(charlieKeyPopover).toHaveCount(0); await charlieNameTrigger.click(); await expect(charlieKeyPopover).toBeVisible(); await expect(charliePubkey).toContainText("npub1"); // The verify popover is npub-only — the raw hex line is gone. await expect(charlieKeyPopover).not.toContainText( TEST_IDENTITIES.charlie.pubkey, ); await waitForAnimations(page); await page.getByTestId("new-message-page").screenshot({ path: `${SHOTS}/new-dm-selected-recipient-key.png`, }); // Full-variant clipboard regression (D1a): the nested copy affordances // must write the recipient's complete canonical npub through the real // bridge — never the legacy raw hex the popover also shows, and never a // truncation. The evidence screenshot above is captured first, so this // interaction leaves it untouched. await page.context().grantPermissions(["clipboard-read", "clipboard-write"]); const copyPublicKeyTrigger = charliePubkey.getByRole("button", { name: "Copy public key", }); await copyPublicKeyTrigger.click(); const copyNpubButton = page.getByRole("button", { name: "Copy npub" }); await copyNpubButton.click(); await expect .poll(() => page.evaluate(() => navigator.clipboard.readText())) .toBe(npubEncode(TEST_IDENTITIES.charlie.pubkey)); await expect( page.locator("[data-sonner-toast]").filter({ hasText: "npub copied" }), ).toBeVisible(); // Copying is not a dismissal: the nested affordance and the inspection // popover it lives in both survive the copy. await expect(copyNpubButton).toBeVisible(); await expect(charlieKeyPopover).toBeVisible(); // The inner Escape closes only the nested key popover — the inspection // stays open. await page.keyboard.press("Escape"); await expect(copyNpubButton).toHaveCount(0); await expect(charlieKeyPopover).toBeVisible(); // Keyboard path: after the inner Escape focus returns naturally to the // full-key trigger; Space reopens the popover, whose auto-focus lands on // Copy npub, and Enter activates it. The sentinel proves this keyboard // copy rewrites the clipboard rather than inheriting the value above. await expect(copyPublicKeyTrigger).toBeFocused(); await page.evaluate(() => navigator.clipboard.writeText("keyboard-copy-sentinel"), ); await page.keyboard.press("Space"); await expect(copyNpubButton).toBeVisible(); await expect(copyNpubButton).toBeFocused(); await page.keyboard.press("Enter"); await expect .poll(() => page.evaluate(() => navigator.clipboard.readText())) .toBe(npubEncode(TEST_IDENTITIES.charlie.pubkey)); // Close the reopened nested popover so the inspection popover owns the // final Escape; the recipient itself survives both dismissals. await page.keyboard.press("Escape"); await expect(copyNpubButton).toHaveCount(0); await page.keyboard.press("Escape"); await expect(charlieKeyPopover).toHaveCount(0); await expect(charlieChip).toBeVisible(); await search.fill("charlie"); await expect(charlieResult).toBeVisible(); await expect(charlieResult).toHaveAttribute( "aria-label", "Already added charlie", ); await charlieName.hover(); await expect(charlieNpub).toHaveCSS("opacity", "1"); await expect(charlieNpub).toHaveText( truncateNpub(TEST_IDENTITIES.charlie.pubkey), ); await expect(charlieName.getByText("charlie", { exact: true })).toHaveCSS( "opacity", "0", ); await page.keyboard.press("Enter"); await expect(search).toHaveValue(""); await expect( page.locator("button[data-testid^='new-dm-selected-']"), ).toHaveCount(1); await page.mouse.move(1_100, 500); await expect(charlieNpub).toHaveCount(0); await expect(charlieKeyPopover).toHaveCount(0); await waitForAnimations(page); await page.getByTestId("new-message-page").screenshot({ path: `${SHOTS}/new-dm-selected-recipient.png`, }); // The To-field guard ignores popover clicks that bubble into the field; // a click on the label itself — a physical descendant of the field — must // still focus the input and open the recipient picker. await page .getByTestId("new-message-to-field") .getByText("To:", { exact: true }) .click(); await expect(search).toBeFocused(); await expect(page.getByTestId("new-message-recipient-popover")).toBeVisible(); }); test("member removal confirm shows the full npub inline", async ({ page }) => { await installMockBridge(page); await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await page.getByTestId("channel-members-trigger").click(); await expect(page.getByTestId("members-sidebar")).toBeVisible(); await waitForAnimations(page); await page.getByTestId("members-sidebar").screenshot({ path: `${SHOTS}/members-sidebar.png`, }); });