Complete filtered user search pages

Fetch another user-search page when selected or archived results leave the visible result viewport underfilled. Cover fully filtered and non-scrollable first pages, and capture duplicate-name UI evidence.

Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
This commit is contained in:
Wes
2026-08-21 09:45:33 -06:00
parent 484b1651e6
commit 5b654ab74f
3 changed files with 121 additions and 38 deletions

View File

@@ -142,6 +142,24 @@ export function CreateAgentRespondToField({
),
[allowlistSet, isArchivedDiscovery, userSearchResults],
);
const searchViewportRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
const viewport = searchViewportRef.current;
if (
!viewport ||
!userSearchQuery.hasNextPage ||
userSearchQuery.isFetchingNextPage ||
viewport.scrollHeight > viewport.clientHeight
) {
return;
}
void userSearchQuery.fetchNextPage();
}, [
userSearchQuery.fetchNextPage,
userSearchQuery.hasNextPage,
userSearchQuery.isFetchingNextPage,
]);
const handleSearchScroll = useUserSearchFetchMoreOnScroll(userSearchQuery);
const pasteParsed = React.useMemo(
@@ -261,6 +279,7 @@ export function CreateAgentRespondToField({
onAddRawPubkey={handleAddRawPubkey}
onAddSearchResult={handleAddSearchResult}
onSearchScroll={handleSearchScroll}
searchViewportRef={searchViewportRef}
onPasteTextChange={setPasteText}
onQueryChange={setQuery}
onRemove={handleRemove}
@@ -296,6 +315,7 @@ function AllowlistPicker({
onAddRawPubkey,
onAddSearchResult,
onSearchScroll,
searchViewportRef,
onPasteTextChange,
onQueryChange,
onRemove,
@@ -318,6 +338,7 @@ function AllowlistPicker({
onAddRawPubkey: (pubkey: string) => void;
onAddSearchResult: (user: UserSearchResult) => void;
onSearchScroll: (event: React.UIEvent<HTMLDivElement>) => void;
searchViewportRef: React.RefObject<HTMLDivElement | null>;
onPasteTextChange: (value: string) => void;
onQueryChange: (value: string) => void;
onRemove: (pubkey: string) => void;
@@ -414,66 +435,69 @@ function AllowlistPicker({
<p className="px-2 py-1 text-sm text-muted-foreground">
Searching…
</p>
) : searchResults.length > 0 ? (
) : (
<div
className="max-h-44 space-y-1 overflow-y-auto"
onScroll={onSearchScroll}
ref={searchViewportRef}
>
{searchResults.map((result) => (
{searchResults.length > 0 ? (
searchResults.map((result) => (
<button
className="flex w-full items-center justify-between rounded-md px-2.5 py-1.5 text-left transition-colors hover:bg-accent hover:text-accent-foreground"
data-testid={`agent-respond-to-result-${result.pubkey}`}
key={result.pubkey}
onClick={() => onAddSearchResult(result)}
type="button"
>
<div className="flex items-center gap-2 min-w-0">
<UserAvatar
avatarUrl={result.avatarUrl}
displayName={formatSearchUserName(result)}
size="xs"
/>
<div className="min-w-0">
<p className="truncate text-sm font-medium leading-5">
{formatSearchUserName(result)}
</p>
<p className="truncate text-xs text-muted-foreground">
{formatSearchUserSecondary(result)}
</p>
</div>
</div>
<span className="text-xs text-muted-foreground">Add</span>
</button>
))
) : queryIsHexPubkey ? (
<button
className="flex w-full items-center justify-between rounded-md px-2.5 py-1.5 text-left transition-colors hover:bg-accent hover:text-accent-foreground"
data-testid={`agent-respond-to-result-${result.pubkey}`}
key={result.pubkey}
onClick={() => onAddSearchResult(result)}
data-testid="agent-respond-to-add-raw-pubkey"
onClick={() => onAddRawPubkey(deferredQuery.toLowerCase())}
type="button"
>
<div className="flex items-center gap-2 min-w-0">
<UserAvatar
avatarUrl={result.avatarUrl}
displayName={formatSearchUserName(result)}
avatarUrl={null}
displayName={truncatePubkey(deferredQuery)}
size="xs"
/>
<div className="min-w-0">
<p className="truncate text-sm font-medium leading-5">
{formatSearchUserName(result)}
{truncatePubkey(deferredQuery)}
</p>
<p className="truncate text-xs text-muted-foreground">
{formatSearchUserSecondary(result)}
Add pubkey directly
</p>
</div>
</div>
<span className="text-xs text-muted-foreground">Add</span>
</button>
))}
) : (
<p className="px-2 py-1 text-sm text-muted-foreground">
No matching users.
</p>
)}
</div>
) : queryIsHexPubkey ? (
<button
className="flex w-full items-center justify-between rounded-md px-2.5 py-1.5 text-left transition-colors hover:bg-accent hover:text-accent-foreground"
data-testid="agent-respond-to-add-raw-pubkey"
onClick={() => onAddRawPubkey(deferredQuery.toLowerCase())}
type="button"
>
<div className="flex items-center gap-2 min-w-0">
<UserAvatar
avatarUrl={null}
displayName={truncatePubkey(deferredQuery)}
size="xs"
/>
<div className="min-w-0">
<p className="truncate text-sm font-medium leading-5">
{truncatePubkey(deferredQuery)}
</p>
<p className="truncate text-xs text-muted-foreground">
Add pubkey directly
</p>
</div>
</div>
<span className="text-xs text-muted-foreground">Add</span>
</button>
) : (
<p className="px-2 py-1 text-sm text-muted-foreground">
No matching users.
</p>
)}
</div>
) : null}

View File

@@ -165,6 +165,9 @@ test("selected-people search keeps same-name people independently selectable", a
await expect(firstWill).toContainText("11111111…1111");
await expect(secondWill).toContainText("Will");
await expect(secondWill).toContainText("22222222…2222");
await page
.getByTestId("agent-respond-to-allowlist")
.screenshot({ path: `${SHOTS}/duplicate-will-selected-people.png` });
await secondWill.click();
await expect(
@@ -173,6 +176,56 @@ test("selected-people search keeps same-name people independently selectable", a
await expect(firstWill).toHaveCount(0);
});
for (const selectedFirstPageCount of [50, 49]) {
test(`selected-people search fetches past a ${
selectedFirstPageCount === 50 ? "fully filtered" : "non-scrollable"
} first page`, async ({ page }) => {
const firstPageProfiles = Array.from({ length: 50 }, (_, index) => ({
pubkey: (index + 1).toString(16).padStart(64, "0"),
displayName: `Will ${String(index).padStart(2, "0")}`,
}));
const targetPubkey = "f".repeat(64);
const agent = TEST_IDENTITIES.charlie;
await installMockBridge(page, {
managedAgents: [
{
pubkey: agent.pubkey,
name: "Hack Day Helper",
status: "running",
channelNames: ["general"],
respondTo: "allowlist",
respondToAllowlist: firstPageProfiles
.slice(0, selectedFirstPageCount)
.map((profile) => profile.pubkey),
},
],
searchProfiles: [
...firstPageProfiles,
{ pubkey: targetPubkey, displayName: "Will Target" },
],
});
await page.goto("/");
await openAgentAccessDialog(page, agent.pubkey);
await page.getByTestId("agent-respond-to-search").fill("Will");
await expect
.poll(
() =>
page.evaluate(() =>
(window.__BUZZ_E2E_COMMAND_LOG__ ?? [])
.filter((entry) => entry.command === "search_users")
.map((entry) => entry.payload),
),
{ message: "search should request the second result page" },
)
.toContainEqual(expect.objectContaining({ cursor: "2" }));
await expect(
page.getByTestId(`agent-respond-to-result-${targetPubkey}`),
).toContainText("Will Target");
});
}
test("full agent editor tightens the exact sidebar agent instance", async ({
page,
}) => {

View File

@@ -8,6 +8,7 @@ import {
const MOCK_VIEWER_PUBKEY = "deadbeef".repeat(8);
const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
const SHOTS = "test-results/mentions";
test.beforeEach(async ({ page }) => {
await installMockBridge(page);
@@ -715,6 +716,11 @@ for (const selectedWill of ["first", "second"] as const) {
await expect(firstWill).toBeVisible();
await expect(secondWill).toBeVisible();
await expect(dropdown.getByTestId("mention-collision-npub")).toHaveCount(2);
if (selectedWill === "first") {
await dropdown.screenshot({
path: `${SHOTS}/duplicate-will-autocomplete.png`,
});
}
await dropdown.getByTestId(`mention-suggestion-${selectedPubkey}`).click();
await page.keyboard.type(`hello ${selectedWill}`);