mirror of
https://github.com/block/buzz.git
synced 2026-11-03 07:51:22 +00:00
feat(desktop): disambiguate agent name collisions
Co-authored-by: npub1ux8n2yfs8qfvgd75s7kyhar2mztac355v6vmrz4juc9l3msw4pgstums9e <e18f3511303812c437d487ac4bf46ad897dc46946699b18ab2e60bf8ee0ea851@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1ux8n2yfs8qfvgd75s7kyhar2mztac355v6vmrz4juc9l3msw4pgstums9e <e18f3511303812c437d487ac4bf46ad897dc46946699b18ab2e60bf8ee0ea851@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Codex <noreply@openai.com> Ai-assisted: true
This commit is contained in:
parent
43bd104de5
commit
df3d388d22
@@ -47,6 +47,7 @@ export default defineConfig({
|
||||
"**/edit-agent.spec.ts",
|
||||
"**/doctor-cta-screenshots.spec.ts",
|
||||
"**/pubkey-display-screenshots.spec.ts",
|
||||
"**/ambiguous-agent-owner-suffix.spec.ts",
|
||||
"**/file-attachment.spec.ts",
|
||||
"**/image-attachment-gallery.spec.ts",
|
||||
"**/composer-image-draw.spec.ts",
|
||||
|
||||
@@ -20,7 +20,12 @@ import {
|
||||
useUserSearchFetchMoreOnScroll,
|
||||
useUsersBatchQuery,
|
||||
} from "@/features/profile/hooks";
|
||||
import { formatOwnerLabel } from "@/features/profile/lib/identity";
|
||||
import {
|
||||
countVisibleDisplayNames,
|
||||
formatDisambiguatedAgentName,
|
||||
formatOwnerLabel,
|
||||
hasVisibleDisplayNameCollision,
|
||||
} from "@/features/profile/lib/identity";
|
||||
import { rankUserCandidatesBySearch } from "@/features/profile/lib/userCandidateSearch";
|
||||
import { usePresenceQuery } from "@/features/presence/hooks";
|
||||
import { useIdentityQuery } from "@/shared/api/hooks";
|
||||
@@ -374,6 +379,13 @@ export function MembersSidebar({
|
||||
userSearchResults,
|
||||
rawMembers,
|
||||
]);
|
||||
const addSearchNameCounts = React.useMemo(
|
||||
() =>
|
||||
countVisibleDisplayNames(
|
||||
addSearchResults.map((user) => formatAddCandidateName(user)),
|
||||
),
|
||||
[addSearchResults],
|
||||
);
|
||||
const isAddSearchLoading =
|
||||
userSearchQuery.isLoading ||
|
||||
managedAgentsQuery.isLoading ||
|
||||
@@ -708,23 +720,39 @@ export function MembersSidebar({
|
||||
Not in this channel
|
||||
</SearchResultSectionTitle>
|
||||
) : null}
|
||||
{addSearchResults.map((user) => (
|
||||
<AddMemberSearchResultRow
|
||||
disabled={
|
||||
addingMemberPubkeys.has(user.pubkey) || isArchived
|
||||
}
|
||||
key={user.pubkey}
|
||||
onSelect={(selectedUser) => {
|
||||
void handleAddSearchResult(selectedUser);
|
||||
}}
|
||||
ownerLabel={formatOwnerLabel(
|
||||
user.ownerPubkey,
|
||||
identityQuery.data?.pubkey,
|
||||
addSearchOwnerProfilesQuery.data?.profiles,
|
||||
)}
|
||||
user={user}
|
||||
/>
|
||||
))}
|
||||
{addSearchResults.map((user) => {
|
||||
const ownerLabel = formatOwnerLabel(
|
||||
user.ownerPubkey,
|
||||
identityQuery.data?.pubkey,
|
||||
addSearchOwnerProfilesQuery.data?.profiles,
|
||||
);
|
||||
const displayName = formatAddCandidateName(user);
|
||||
|
||||
return (
|
||||
<AddMemberSearchResultRow
|
||||
disabled={
|
||||
addingMemberPubkeys.has(user.pubkey) ||
|
||||
isArchived
|
||||
}
|
||||
displayName={formatDisambiguatedAgentName({
|
||||
displayName,
|
||||
hasNameCollision:
|
||||
hasVisibleDisplayNameCollision(
|
||||
displayName,
|
||||
addSearchNameCounts,
|
||||
),
|
||||
isAgent: user.isAgent,
|
||||
ownerLabel,
|
||||
})}
|
||||
key={user.pubkey}
|
||||
onSelect={(selectedUser) => {
|
||||
void handleAddSearchResult(selectedUser);
|
||||
}}
|
||||
ownerLabel={ownerLabel}
|
||||
user={user}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{isAddSearchLoading ? (
|
||||
<p className="px-4 py-3 text-sm text-muted-foreground">
|
||||
Searching...
|
||||
@@ -851,11 +879,13 @@ function SearchResultSectionTitle({
|
||||
|
||||
function AddMemberSearchResultRow({
|
||||
disabled,
|
||||
displayName,
|
||||
onSelect,
|
||||
ownerLabel,
|
||||
user,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
displayName: string;
|
||||
onSelect: (user: UserSearchResult) => void;
|
||||
ownerLabel?: string | null;
|
||||
user: UserSearchResult;
|
||||
@@ -869,7 +899,7 @@ function AddMemberSearchResultRow({
|
||||
data-testid={`channel-user-search-result-${user.pubkey}`}
|
||||
>
|
||||
<button
|
||||
aria-label={`Select ${formatAddCandidateName(user)}`}
|
||||
aria-label={`Select ${displayName}`}
|
||||
className="absolute inset-0 z-0 cursor-pointer focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"
|
||||
disabled={disabled}
|
||||
onClick={() => onSelect(user)}
|
||||
@@ -885,8 +915,11 @@ function AddMemberSearchResultRow({
|
||||
{user.isAgent ? (
|
||||
<div className="min-w-0">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate text-sm font-medium tracking-tight">
|
||||
{formatAddCandidateName(user)}
|
||||
<span
|
||||
className="truncate text-sm font-medium tracking-tight"
|
||||
data-testid="channel-user-search-result-name"
|
||||
>
|
||||
{displayName}
|
||||
</span>
|
||||
<span className="inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground">
|
||||
<Bot aria-hidden="true" className="h-4 w-4" />
|
||||
|
||||
@@ -12,6 +12,11 @@ import {
|
||||
import { UserAvatar } from "@/shared/ui/UserAvatar";
|
||||
import { safeNpub } from "@/shared/lib/nostrUtils";
|
||||
import { truncatePubkey } from "@/shared/lib/pubkey";
|
||||
import {
|
||||
countVisibleDisplayNames,
|
||||
formatDisambiguatedAgentName,
|
||||
hasVisibleDisplayNameCollision,
|
||||
} from "@/features/profile/lib/identity";
|
||||
|
||||
export type MentionSuggestion = {
|
||||
pubkey?: string;
|
||||
@@ -66,12 +71,11 @@ export const MentionAutocomplete = React.memo(function MentionAutocomplete({
|
||||
|
||||
// Name collisions are the impersonation vector: a vanity-ground key can
|
||||
// wear any display name. When two suggestions share a name, surface each
|
||||
// one's npub (truncated; full key in the hover tooltip) to tell them apart.
|
||||
const nameCounts = new Map<string, number>();
|
||||
for (const suggestion of suggestions) {
|
||||
const name = suggestion.displayName.toLowerCase();
|
||||
nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1);
|
||||
}
|
||||
// one's owner in the title (when known) and npub (truncated; full key in the
|
||||
// hover tooltip) to tell them apart.
|
||||
const nameCounts = countVisibleDisplayNames(
|
||||
suggestions.map((suggestion) => suggestion.displayName),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -101,8 +105,16 @@ export const MentionAutocomplete = React.memo(function MentionAutocomplete({
|
||||
(suggestion.teamId ? `team-${suggestion.teamId}` : null) ??
|
||||
suggestion.displayName;
|
||||
const agentLabel = "agent";
|
||||
const hasNameCollision =
|
||||
(nameCounts.get(suggestion.displayName.toLowerCase()) ?? 0) > 1;
|
||||
const hasNameCollision = hasVisibleDisplayNameCollision(
|
||||
suggestion.displayName,
|
||||
nameCounts,
|
||||
);
|
||||
const displayName = formatDisambiguatedAgentName({
|
||||
displayName: suggestion.displayName,
|
||||
hasNameCollision,
|
||||
isAgent: suggestion.isAgent,
|
||||
ownerLabel: suggestion.ownerLabel,
|
||||
});
|
||||
const collisionNpub =
|
||||
hasNameCollision && suggestion.pubkey
|
||||
? safeNpub(suggestion.pubkey)
|
||||
@@ -140,9 +152,10 @@ export const MentionAutocomplete = React.memo(function MentionAutocomplete({
|
||||
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<span
|
||||
className="min-w-0 break-words font-medium leading-snug"
|
||||
title={suggestion.displayName}
|
||||
data-testid="mention-suggestion-name"
|
||||
title={displayName}
|
||||
>
|
||||
{suggestion.displayName}
|
||||
{displayName}
|
||||
</span>
|
||||
{suggestion.kind === "team" ||
|
||||
suggestion.isAgent ||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Bot } from "lucide-react";
|
||||
|
||||
import { formatOwnerLabel } from "@/features/profile/lib/identity";
|
||||
import {
|
||||
formatDisambiguatedAgentName,
|
||||
formatOwnerLabel,
|
||||
} from "@/features/profile/lib/identity";
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
|
||||
import type { UserSearchResult } from "@/shared/api/types";
|
||||
@@ -67,6 +70,7 @@ function HoverRecipientIdentity({
|
||||
export function NewMessageResultRow({
|
||||
currentPubkey,
|
||||
disabled,
|
||||
hasNameCollision = false,
|
||||
isAlreadySelected = false,
|
||||
isKeyboardHighlighted = false,
|
||||
onSelect,
|
||||
@@ -75,6 +79,7 @@ export function NewMessageResultRow({
|
||||
}: {
|
||||
currentPubkey?: string;
|
||||
disabled: boolean;
|
||||
hasNameCollision?: boolean;
|
||||
isAlreadySelected?: boolean;
|
||||
isKeyboardHighlighted?: boolean;
|
||||
onSelect: (user: UserSearchResult) => void;
|
||||
@@ -87,6 +92,12 @@ export function NewMessageResultRow({
|
||||
currentPubkey,
|
||||
ownerProfiles,
|
||||
);
|
||||
const displayName = formatDisambiguatedAgentName({
|
||||
displayName: name,
|
||||
hasNameCollision,
|
||||
isAgent: user.isAgent,
|
||||
ownerLabel,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -94,7 +105,7 @@ export function NewMessageResultRow({
|
||||
data-keyboard-highlighted={isKeyboardHighlighted ? "true" : undefined}
|
||||
>
|
||||
<button
|
||||
aria-label={`${isAlreadySelected ? "Already added" : "Add"} ${name}`}
|
||||
aria-label={`${isAlreadySelected ? "Already added" : "Add"} ${displayName}`}
|
||||
aria-selected={isAlreadySelected || isKeyboardHighlighted}
|
||||
className={cn(
|
||||
"group/dm-result flex min-h-14 w-full cursor-pointer items-center gap-3 px-4 py-3.5 text-left transition-colors duration-150 ease-out hover:bg-muted/40 focus-visible:bg-muted/40 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-60",
|
||||
@@ -120,7 +131,7 @@ export function NewMessageResultRow({
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div className="flex min-w-0 flex-1">
|
||||
<HoverRecipientIdentity
|
||||
displayName={name}
|
||||
displayName={displayName}
|
||||
pubkey={user.pubkey}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
} from "@/features/channels/hooks";
|
||||
import type { Channel } from "@/shared/api/types";
|
||||
import { useSendMessageMutation } from "@/features/messages/hooks";
|
||||
import {
|
||||
countVisibleDisplayNames,
|
||||
hasVisibleDisplayNameCollision,
|
||||
} from "@/features/profile/lib/identity";
|
||||
import { getKeyboardSearchSelection } from "@/features/profile/lib/userCandidateSearch";
|
||||
import { SelectedRecipientChip } from "@/features/profile/ui/SelectedRecipientChip";
|
||||
import { useIdentityQuery } from "@/shared/api/hooks";
|
||||
@@ -72,6 +76,13 @@ export function NewMessageScreen() {
|
||||
const isSearchTransitionPending = searchQuery.trim() !== deferredSearchQuery;
|
||||
const visibleSearchResults =
|
||||
isSearchTransitionPending || isDirectoryLoading ? [] : searchResults;
|
||||
const visibleSearchNameCounts = React.useMemo(
|
||||
() =>
|
||||
countVisibleDisplayNames(
|
||||
visibleSearchResults.map((user) => formatRecipientName(user)),
|
||||
),
|
||||
[visibleSearchResults],
|
||||
);
|
||||
const showRecipientPicker = isRecipientPickerOpen && !isPending;
|
||||
const highlightedRecipientIndex = React.useMemo(() => {
|
||||
if (!showRecipientPicker || visibleSearchResults.length === 0) {
|
||||
@@ -518,6 +529,10 @@ export function NewMessageScreen() {
|
||||
isKeyboardHighlighted={
|
||||
highlightedRecipient?.pubkey === user.pubkey
|
||||
}
|
||||
hasNameCollision={hasVisibleDisplayNameCollision(
|
||||
formatRecipientName(user),
|
||||
visibleSearchNameCounts,
|
||||
)}
|
||||
key={user.pubkey}
|
||||
onSelect={handleResultSelect}
|
||||
ownerProfiles={ownerProfiles}
|
||||
|
||||
@@ -1,7 +1,61 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { profileLookupsEqual } from "./identity.ts";
|
||||
import {
|
||||
countVisibleDisplayNames,
|
||||
formatDisambiguatedAgentName,
|
||||
hasVisibleDisplayNameCollision,
|
||||
profileLookupsEqual,
|
||||
} from "./identity.ts";
|
||||
|
||||
test("formatDisambiguatedAgentName appends an owner for colliding agents", () => {
|
||||
assert.equal(
|
||||
formatDisambiguatedAgentName({
|
||||
displayName: "Bumble",
|
||||
hasNameCollision: true,
|
||||
isAgent: true,
|
||||
ownerLabel: "sergior",
|
||||
}),
|
||||
"Bumble (sergior)",
|
||||
);
|
||||
assert.equal(
|
||||
formatDisambiguatedAgentName({
|
||||
displayName: "Bumble",
|
||||
hasNameCollision: true,
|
||||
isAgent: true,
|
||||
ownerLabel: "you",
|
||||
}),
|
||||
"Bumble (you)",
|
||||
);
|
||||
});
|
||||
|
||||
test("formatDisambiguatedAgentName leaves humans and unique agents unchanged", () => {
|
||||
assert.equal(
|
||||
formatDisambiguatedAgentName({
|
||||
displayName: "Bumble",
|
||||
hasNameCollision: true,
|
||||
isAgent: false,
|
||||
ownerLabel: null,
|
||||
}),
|
||||
"Bumble",
|
||||
);
|
||||
assert.equal(
|
||||
formatDisambiguatedAgentName({
|
||||
displayName: "Bumble",
|
||||
hasNameCollision: false,
|
||||
isAgent: true,
|
||||
ownerLabel: "sergior",
|
||||
}),
|
||||
"Bumble",
|
||||
);
|
||||
});
|
||||
|
||||
test("visible display-name collisions are case-insensitive", () => {
|
||||
const counts = countVisibleDisplayNames(["Bumble", " bUmBlE ", "Fizz"]);
|
||||
|
||||
assert.equal(hasVisibleDisplayNameCollision("bumble", counts), true);
|
||||
assert.equal(hasVisibleDisplayNameCollision("Fizz", counts), false);
|
||||
});
|
||||
|
||||
const summary = (over = {}) => ({
|
||||
displayName: "Ada",
|
||||
|
||||
@@ -191,3 +191,48 @@ export function formatOwnerLabel(
|
||||
truncatePubkey(ownerPubkey)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an agent's owner to a display name only when that name collides with
|
||||
* another visible result. The returned label is presentation-only; callers
|
||||
* keep the original display name for selection and mention insertion.
|
||||
*/
|
||||
export function formatDisambiguatedAgentName(input: {
|
||||
displayName: string;
|
||||
hasNameCollision: boolean;
|
||||
isAgent?: boolean;
|
||||
ownerLabel?: string | null;
|
||||
}) {
|
||||
const { displayName, hasNameCollision, isAgent, ownerLabel } = input;
|
||||
const normalizedOwnerLabel = ownerLabel?.trim();
|
||||
|
||||
if (!hasNameCollision || !isAgent || !normalizedOwnerLabel) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
return `${displayName} (${normalizedOwnerLabel})`;
|
||||
}
|
||||
|
||||
/** Case-insensitive display-name counts for a currently visible result set. */
|
||||
export function countVisibleDisplayNames(
|
||||
displayNames: readonly (string | null | undefined)[],
|
||||
) {
|
||||
const counts = new Map<string, number>();
|
||||
|
||||
for (const displayName of displayNames) {
|
||||
const normalizedName = displayName?.trim().toLowerCase();
|
||||
if (!normalizedName) {
|
||||
continue;
|
||||
}
|
||||
counts.set(normalizedName, (counts.get(normalizedName) ?? 0) + 1);
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
export function hasVisibleDisplayNameCollision(
|
||||
displayName: string,
|
||||
counts: ReadonlyMap<string, number>,
|
||||
) {
|
||||
return (counts.get(displayName.trim().toLowerCase()) ?? 0) > 1;
|
||||
}
|
||||
|
||||
151
desktop/tests/e2e/ambiguous-agent-owner-suffix.spec.ts
Normal file
151
desktop/tests/e2e/ambiguous-agent-owner-suffix.spec.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import {
|
||||
installMockBridge,
|
||||
openNewMessagePage,
|
||||
TEST_IDENTITIES,
|
||||
} from "../helpers/bridge";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
|
||||
const SHOTS = "test-results/ambiguous-agent-owner-suffix";
|
||||
const CURRENT_OWNER_PUBKEY = "deadbeef".repeat(8);
|
||||
const FIRST_AGENT_PUBKEY = "1010".repeat(16);
|
||||
const SECOND_AGENT_PUBKEY = "2020".repeat(16);
|
||||
|
||||
const duplicateAgentOptions = {
|
||||
relayAgents: [
|
||||
{
|
||||
pubkey: FIRST_AGENT_PUBKEY,
|
||||
name: "Echo",
|
||||
respondTo: "allowlist" as const,
|
||||
respondToAllowlist: [CURRENT_OWNER_PUBKEY],
|
||||
},
|
||||
{
|
||||
pubkey: SECOND_AGENT_PUBKEY,
|
||||
name: "eChO",
|
||||
respondTo: "allowlist" as const,
|
||||
respondToAllowlist: [CURRENT_OWNER_PUBKEY],
|
||||
},
|
||||
],
|
||||
searchProfiles: [
|
||||
{
|
||||
pubkey: FIRST_AGENT_PUBKEY,
|
||||
displayName: "Echo",
|
||||
ownerPubkey: CURRENT_OWNER_PUBKEY,
|
||||
isAgent: true,
|
||||
},
|
||||
{
|
||||
pubkey: SECOND_AGENT_PUBKEY,
|
||||
displayName: "eChO",
|
||||
ownerPubkey: TEST_IDENTITIES.outsider.pubkey,
|
||||
isAgent: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
test("captures duplicate agent names in mention autocomplete", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, duplicateAgentOptions);
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await page.getByTestId("message-input").fill("@echo");
|
||||
|
||||
const autocomplete = page
|
||||
.getByTestId("message-composer")
|
||||
.getByTestId("mention-autocomplete");
|
||||
const firstRow = autocomplete.getByTestId(
|
||||
`mention-suggestion-${FIRST_AGENT_PUBKEY}`,
|
||||
);
|
||||
const secondRow = autocomplete.getByTestId(
|
||||
`mention-suggestion-${SECOND_AGENT_PUBKEY}`,
|
||||
);
|
||||
await expect(firstRow.getByTestId("mention-suggestion-name")).toHaveText(
|
||||
"Echo (you)",
|
||||
);
|
||||
await expect(secondRow.getByTestId("mention-suggestion-name")).toHaveText(
|
||||
"eChO (outsider)",
|
||||
);
|
||||
await expect(firstRow).toContainText("owned by you");
|
||||
await expect(secondRow).toContainText("owned by outsider");
|
||||
|
||||
await waitForAnimations(page);
|
||||
await autocomplete.screenshot({ path: `${SHOTS}/after-mention.png` });
|
||||
});
|
||||
|
||||
test("captures duplicate agent names in add-member results", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, duplicateAgentOptions);
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await page.getByTestId("channel-members-trigger").click();
|
||||
await page.getByTestId("channel-management-search-users").fill("echo");
|
||||
|
||||
const firstRow = page.getByTestId(
|
||||
`channel-user-search-result-${FIRST_AGENT_PUBKEY}`,
|
||||
);
|
||||
const secondRow = page.getByTestId(
|
||||
`channel-user-search-result-${SECOND_AGENT_PUBKEY}`,
|
||||
);
|
||||
await expect(
|
||||
firstRow.getByTestId("channel-user-search-result-name"),
|
||||
).toHaveText("Echo (you)");
|
||||
await expect(
|
||||
secondRow.getByTestId("channel-user-search-result-name"),
|
||||
).toHaveText("eChO (outsider)");
|
||||
await expect(
|
||||
firstRow.getByRole("button", { name: "Select Echo (you)", exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
secondRow.getByRole("button", {
|
||||
name: "Select eChO (outsider)",
|
||||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(firstRow).toContainText("owned by you");
|
||||
await expect(secondRow).toContainText("owned by outsider");
|
||||
|
||||
await waitForAnimations(page);
|
||||
await page
|
||||
.getByTestId("members-sidebar-people")
|
||||
.screenshot({ path: `${SHOTS}/after-add-member.png` });
|
||||
});
|
||||
|
||||
test("captures duplicate agent names in new-message results", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, duplicateAgentOptions);
|
||||
await page.goto("/");
|
||||
await openNewMessagePage(page);
|
||||
await page.getByTestId("new-dm-search").fill("echo");
|
||||
|
||||
const firstRow = page.getByTestId(`new-dm-result-${FIRST_AGENT_PUBKEY}`);
|
||||
const secondRow = page.getByTestId(`new-dm-result-${SECOND_AGENT_PUBKEY}`);
|
||||
await expect(
|
||||
firstRow
|
||||
.getByTestId(`new-dm-name-${FIRST_AGENT_PUBKEY}`)
|
||||
.getByText("Echo (you)", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
secondRow
|
||||
.getByTestId(`new-dm-name-${SECOND_AGENT_PUBKEY}`)
|
||||
.getByText("eChO (outsider)", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("option", { name: "Add Echo (you)", exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("option", {
|
||||
name: "Add eChO (outsider)",
|
||||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(firstRow).toContainText("owned by you");
|
||||
await expect(secondRow).toContainText("owned by outsider");
|
||||
|
||||
await waitForAnimations(page);
|
||||
await page
|
||||
.getByTestId("new-message-recipient-popover")
|
||||
.screenshot({ path: `${SHOTS}/after-new-message.png` });
|
||||
});
|
||||
Reference in New Issue
Block a user