feat: add inline global search topbar

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Thomas Petersen
2026-05-24 21:18:59 -04:00
co-authored by Cursor
parent 2a8a3dccb6
commit 33a63bded5
8 changed files with 482 additions and 126 deletions
+54 -38
View File
@@ -1,58 +1,74 @@
import { ChevronLeft, ChevronRight, Search } from "lucide-react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { TopbarSearch } from "@/features/search/ui/TopbarSearch";
import { Button } from "@/shared/ui/button";
import type { Channel, SearchHit } from "@/shared/api/types";
import { SidebarTrigger } from "@/shared/ui/sidebar";
type AppHeaderControlsProps = {
canGoBack: boolean;
canGoForward: boolean;
channels: Channel[];
currentPubkey?: string;
onGoBack: () => void;
onGoForward: () => void;
onOpenSearch: () => void;
onOpenChannel: (channelId: string) => void;
onOpenResult: (hit: SearchHit) => void;
onOpenUser: (pubkey: string) => void;
};
export function AppHeaderControls({
canGoBack,
canGoForward,
channels,
currentPubkey,
onGoBack,
onGoForward,
onOpenSearch,
onOpenChannel,
onOpenResult,
onOpenUser,
}: AppHeaderControlsProps) {
return (
<div className="fixed left-[80px] top-[9px] z-50 flex items-center gap-0.5">
<SidebarTrigger className="h-[22px] w-[22px] text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground" />
<Button
aria-label="Go back"
className="h-[22px] w-[22px] text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"
data-testid="global-back"
disabled={!canGoBack}
onClick={onGoBack}
size="icon"
variant="ghost"
>
<ChevronLeft className="h-3 w-3" />
</Button>
<Button
aria-label="Go forward"
className="h-[22px] w-[22px] text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"
data-testid="global-forward"
disabled={!canGoForward}
onClick={onGoForward}
size="icon"
variant="ghost"
>
<ChevronRight className="h-3 w-3" />
</Button>
<Button
aria-label="Search Sprout"
className="h-[22px] w-[22px] text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"
data-testid="global-search"
onClick={onOpenSearch}
size="icon"
variant="ghost"
>
<Search className="h-3 w-3" />
</Button>
</div>
<>
<div className="pointer-events-none fixed inset-x-0 top-0 z-20 h-20 bg-background/35 backdrop-blur-2xl backdrop-saturate-150 [mask-image:linear-gradient(to_bottom,black_0%,black_45%,transparent_100%)] supports-[backdrop-filter]:bg-background/25 [-webkit-mask-image:linear-gradient(to_bottom,black_0%,black_45%,transparent_100%)]" />
<div className="pointer-events-none fixed inset-x-0 top-0 z-[70] h-10">
<div className="pointer-events-auto absolute left-[80px] top-[9px] flex items-center gap-0.5">
<SidebarTrigger className="h-[22px] w-[22px] text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground" />
<Button
aria-label="Go back"
className="h-[22px] w-[22px] text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"
data-testid="global-back"
disabled={!canGoBack}
onClick={onGoBack}
size="icon"
variant="ghost"
>
<ChevronLeft className="h-3 w-3" />
</Button>
<Button
aria-label="Go forward"
className="h-[22px] w-[22px] text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"
data-testid="global-forward"
disabled={!canGoForward}
onClick={onGoForward}
size="icon"
variant="ghost"
>
<ChevronRight className="h-3 w-3" />
</Button>
</div>
<div className="pointer-events-auto absolute left-1/2 top-[7px] w-[min(420px,42vw)] -translate-x-1/2">
<TopbarSearch
channels={channels}
currentPubkey={currentPubkey}
onOpenChannel={onOpenChannel}
onOpenResult={onOpenResult}
onOpenUser={onOpenUser}
/>
</div>
</div>
</>
);
}
+22 -26
View File
@@ -169,6 +169,7 @@ export function AppShell() {
const [isChannelManagementOpen, setIsChannelManagementOpen] =
React.useState(false);
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
const [searchInitialQuery, setSearchInitialQuery] = React.useState("");
const [browseDialogType, setBrowseDialogType] =
React.useState<BrowseDialogType>(null);
const [isNewDmOpen, setIsNewDmOpen] = React.useState(false);
@@ -313,10 +314,14 @@ export function AppShell() {
setBrowseDialogType("forum");
void refetchChannels();
}, [refetchChannels]);
const handleOpenSearch = React.useCallback(() => {
setIsSearchOpen(true);
void refetchChannels();
}, [refetchChannels]);
const handleOpenSearch = React.useCallback(
(query = "") => {
setSearchInitialQuery(query);
setIsSearchOpen(true);
void refetchChannels();
},
[refetchChannels],
);
const handleBrowseDialogOpenChange = React.useCallback((open: boolean) => {
if (!open) {
@@ -617,9 +622,13 @@ export function AppShell() {
<AppHeaderControls
canGoBack={canGoBack}
canGoForward={canGoForward}
channels={channels}
currentPubkey={identityQuery.data?.pubkey}
onGoBack={goBack}
onGoForward={goForward}
onOpenSearch={handleOpenSearch}
onOpenChannel={(channelId) => void goChannel(channelId)}
onOpenResult={handleOpenSearchResult}
onOpenUser={handleOpenSearchUser}
/>
<AppSidebar
activeWorkspace={workspacesHook.activeWorkspace}
@@ -700,25 +709,13 @@ export function AppShell() {
});
await goChannel(directMessage.id);
}}
onSelectAgents={() => {
void goAgents();
}}
onSelectChannel={(channelId) => {
void goChannel(channelId);
}}
onSelectHome={() => {
void goHome();
}}
onSelectProjects={() => {
void goProjects();
}}
onSelectPulse={() => {
void goPulse();
}}
onSelectAgents={() => void goAgents()}
onSelectChannel={(channelId) => void goChannel(channelId)}
onSelectHome={() => void goHome()}
onSelectProjects={() => void goProjects()}
onSelectPulse={() => void goPulse()}
onSelectSettings={handleOpenSettings}
onSelectWorkflows={() => {
void goWorkflows();
}}
onSelectWorkflows={() => void goWorkflows()}
onSetPresenceStatus={(status) =>
presenceSession.setStatus(status)
}
@@ -751,6 +748,7 @@ export function AppShell() {
currentPubkey={identityQuery.data?.pubkey}
isChannelManagementOpen={isChannelManagementOpen}
isSearchOpen={isSearchOpen}
searchInitialQuery={searchInitialQuery}
onBrowseChannelJoin={handleBrowseChannelJoin}
onBrowseDialogOpenChange={handleBrowseDialogOpenChange}
onChannelManagementOpenChange={setIsChannelManagementOpen}
@@ -761,9 +759,7 @@ export function AppShell() {
onOpenSearchResult={handleOpenSearchResult}
onOpenSearchUser={handleOpenSearchUser}
onSearchOpenChange={setIsSearchOpen}
onSelectChannel={(channelId) => {
void goChannel(channelId);
}}
onSelectChannel={(channelId) => void goChannel(channelId)}
/>
{settingsOpen ? (
+3
View File
@@ -26,6 +26,7 @@ type AppShellOverlaysProps = {
currentPubkey?: string;
isChannelManagementOpen: boolean;
isSearchOpen: boolean;
searchInitialQuery: string;
onBrowseChannelJoin: (channelId: string) => Promise<void>;
onBrowseDialogOpenChange: (open: boolean) => void;
onChannelManagementOpenChange: (open: boolean) => void;
@@ -43,6 +44,7 @@ export function AppShellOverlays({
currentPubkey,
isChannelManagementOpen,
isSearchOpen,
searchInitialQuery,
onBrowseChannelJoin,
onBrowseDialogOpenChange,
onChannelManagementOpenChange,
@@ -72,6 +74,7 @@ export function AppShellOverlays({
<SearchDialog
channels={channels}
currentPubkey={currentPubkey}
initialQuery={searchInitialQuery}
onOpenChannel={onSelectChannel}
onOpenResult={onOpenSearchResult}
onOpenUser={onOpenSearchUser}
+53 -39
View File
@@ -10,11 +10,11 @@ import {
Zap,
} from "lucide-react";
import type * as React from "react";
import { createPortal } from "react-dom";
import type { ChannelType, ChannelVisibility } from "@/shared/api/types";
import { UpdateIndicator } from "@/features/settings/UpdateIndicator";
import { cn } from "@/shared/lib/cn";
import { useSidebar } from "@/shared/ui/sidebar";
type ChatHeaderProps = {
actions?: React.ReactNode;
@@ -27,7 +27,7 @@ type ChatHeaderProps = {
statusBadge?: React.ReactNode;
};
const HEADER_ICON_CLASS = "h-[14px] w-[14px] text-muted-foreground";
const HEADER_ICON_CLASS = "h-[14px] w-[14px] text-foreground";
function ChannelIcon({
channelType,
@@ -84,45 +84,59 @@ export function ChatHeader({
statusBadge,
}: ChatHeaderProps) {
const trimmedDescription = description?.trim() ?? "";
const { state: sidebarState } = useSidebar();
const reserveGlobalControls = sidebarState === "collapsed";
const actionCluster = (
<div className="flex shrink-0 items-center gap-1">
<UpdateIndicator />
{actions ? <div className="shrink-0">{actions}</div> : null}
</div>
);
const topbarActions =
overlaysContent && typeof document !== "undefined"
? createPortal(
<div className="fixed right-3 top-[7px] z-[60]">{actionCluster}</div>,
document.body,
)
: null;
return (
<header
className={cn(
"relative z-30 flex min-h-[44px] min-w-0 shrink-0 cursor-default select-none items-center gap-[10px] bg-background/70 py-[6px] pl-[16px] pr-[8px] backdrop-blur-xl transition-[margin,padding] duration-200 ease-linear supports-[backdrop-filter]:bg-background/55 sm:pl-[24px] sm:pr-[12px]",
overlaysContent && "-mb-[44px]",
reserveGlobalControls && "md:pl-[160px]",
)}
data-testid="chat-header"
data-tauri-drag-region
>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 flex-wrap items-center gap-[4px]">
<ChannelIcon
channelType={channelType}
mode={mode}
visibility={visibility}
/>
<h1
className="min-w-0 truncate text-sm font-semibold leading-5 tracking-tight"
data-testid="chat-title"
title={trimmedDescription || undefined}
>
{title}
</h1>
{statusBadge ? (
<div className="flex shrink-0 flex-wrap items-center gap-1">
{statusBadge}
</div>
) : null}
</div>
</div>
<>
{topbarActions}
<header
className={cn(
"relative z-30 flex min-h-[44px] min-w-0 shrink-0 cursor-default select-none items-center gap-[10px] py-[6px] pl-[16px] pr-[8px] transition-[margin,padding] duration-200 ease-linear sm:pl-[24px] sm:pr-[12px]",
overlaysContent && "-mb-[44px]",
)}
data-testid="chat-header"
data-tauri-drag-region
>
<div className="pointer-events-none absolute inset-0 bg-background/70 backdrop-blur-xl [mask-image:linear-gradient(to_bottom,black_0%,transparent_100%)] supports-[backdrop-filter]:bg-background/55 [-webkit-mask-image:linear-gradient(to_bottom,black_0%,transparent_100%)]" />
<div className="flex shrink-0 items-center gap-1">
<UpdateIndicator />
{actions ? <div className="shrink-0">{actions}</div> : null}
</div>
</header>
<div className="relative z-10 min-w-0 flex-1">
<div className="flex min-w-0 flex-wrap items-center gap-[4px]">
<ChannelIcon
channelType={channelType}
mode={mode}
visibility={visibility}
/>
<h1
className="min-w-0 truncate text-sm font-semibold leading-5 tracking-tight text-foreground"
data-testid="chat-title"
title={trimmedDescription || undefined}
>
{title}
</h1>
{statusBadge ? (
<div className="flex shrink-0 flex-wrap items-center gap-1">
{statusBadge}
</div>
) : null}
</div>
</div>
{overlaysContent ? null : (
<div className="relative z-10">{actionCluster}</div>
)}
</header>
</>
);
}
@@ -153,11 +153,7 @@ export function MessageThreadPanel({
<>
{isOverlay && <OverlayPanelBackdrop onClose={onClose} />}
<aside
className={cn(
PANEL_BASE_CLASS,
!isOverlay && "pt-11",
isOverlay && PANEL_OVERLAY_CLASS,
)}
className={cn(PANEL_BASE_CLASS, isOverlay && PANEL_OVERLAY_CLASS)}
data-testid="message-thread-panel"
style={{ width: `${widthPx}px` }}
>
@@ -179,12 +175,15 @@ export function MessageThreadPanel({
</button>
)}
<div className="flex items-center gap-3 px-4 py-3">
<div className="pointer-events-none absolute inset-x-0 top-0 z-30 flex min-h-[44px] items-center gap-3 px-4 py-[6px]">
<div className="min-w-0 flex-1">
<h2 className="text-sm font-semibold tracking-tight">Thread</h2>
<h2 className="text-sm font-semibold tracking-tight text-foreground">
Thread
</h2>
</div>
<Button
aria-label="Close thread"
className="pointer-events-auto h-7 w-7"
data-testid="message-thread-close"
onClick={onClose}
size="icon"
@@ -196,7 +195,7 @@ export function MessageThreadPanel({
</div>
<div
className="min-h-0 flex-1 overflow-y-auto pb-24"
className="min-h-0 flex-1 overflow-y-auto pb-24 pt-11"
data-testid="message-thread-body"
onScroll={syncScrollState}
ref={threadBodyRef}
@@ -78,6 +78,7 @@ function SearchLoadingState() {
type SearchDialogProps = {
channels: Channel[];
currentPubkey?: string;
initialQuery?: string;
open: boolean;
onOpenChange: (open: boolean) => void;
onOpenChannel: (channelId: string) => void;
@@ -94,6 +95,7 @@ type SearchResultSection = {
export function SearchDialog({
channels,
currentPubkey,
initialQuery = "",
open,
onOpenChange,
onOpenChannel,
@@ -240,6 +242,15 @@ export function SearchDialog({
}
}, [open]);
React.useEffect(() => {
if (!open) {
return;
}
setQuery(initialQuery);
setSelectedIndex(0);
}, [initialQuery, open]);
React.useEffect(() => {
setSelectedIndex((current) => {
if (results.length === 0) {
@@ -0,0 +1,307 @@
import * as React from "react";
import { Search } from "lucide-react";
import {
useUserSearchQuery,
useUsersBatchQuery,
} from "@/features/profile/hooks";
import { useSearchMessagesQuery } from "@/features/search/hooks";
import {
ChannelResultBody,
MessageResultBody,
resultIcon,
resultKey,
resultTestId,
SearchResultShell,
type SearchResult,
UserResultAvatar,
UserResultBody,
} from "@/features/search/ui/SearchResultItem";
import type { Channel, SearchHit } from "@/shared/api/types";
import { Input } from "@/shared/ui/input";
const MIN_QUERY_LENGTH = 2;
type SearchResultSection = {
count: number;
results: SearchResult[];
title: string;
};
type TopbarSearchProps = {
channels: Channel[];
currentPubkey?: string;
onOpenChannel: (channelId: string) => void;
onOpenResult: (hit: SearchHit) => void;
onOpenUser: (pubkey: string) => void;
};
export function TopbarSearch({
channels,
currentPubkey,
onOpenChannel,
onOpenResult,
onOpenUser,
}: TopbarSearchProps) {
const [query, setQuery] = React.useState("");
const [selectedIndex, setSelectedIndex] = React.useState(0);
const [isFocused, setIsFocused] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);
const trimmedQuery = query.trim();
const shouldSearch = isFocused && trimmedQuery.length >= MIN_QUERY_LENGTH;
const channelLookup = React.useMemo(
() => new Map(channels.map((channel) => [channel.id, channel])),
[channels],
);
const searchQuery = useSearchMessagesQuery(trimmedQuery, {
enabled: shouldSearch,
limit: 8,
});
const usersQuery = useUserSearchQuery(trimmedQuery, {
enabled: shouldSearch,
limit: 4,
});
const messageResults = searchQuery.data?.hits ?? [];
const userResults = React.useMemo(
() =>
(usersQuery.data ?? []).filter(
(user) => user.pubkey.toLowerCase() !== currentPubkey?.toLowerCase(),
),
[currentPubkey, usersQuery.data],
);
const channelResults = React.useMemo(() => {
if (!shouldSearch) {
return [];
}
const normalizedQuery = trimmedQuery.toLowerCase();
return channels
.filter(
(channel) =>
channel.channelType !== "dm" &&
(channel.archivedAt
? channel.isMember
: channel.visibility === "open" || channel.isMember) &&
(channel.name.toLowerCase().includes(normalizedQuery) ||
channel.description.toLowerCase().includes(normalizedQuery)),
)
.sort((a, b) => {
const aNameMatches = a.name.toLowerCase().includes(normalizedQuery);
const bNameMatches = b.name.toLowerCase().includes(normalizedQuery);
if (aNameMatches !== bNameMatches) {
return aNameMatches ? -1 : 1;
}
return a.name.localeCompare(b.name);
})
.slice(0, 4);
}, [channels, shouldSearch, trimmedQuery]);
const sections = React.useMemo<SearchResultSection[]>(
() =>
[
{
count: userResults.length,
results: userResults.map((user) => ({
kind: "user" as const,
user,
})),
title: "People",
},
{
count: channelResults.length,
results: channelResults.map((channel) => ({
kind: "channel" as const,
channel,
})),
title: "Channels",
},
{
count: searchQuery.data?.found ?? messageResults.length,
results: messageResults.map((hit) => ({
kind: "message" as const,
hit,
})),
title: "Messages",
},
].filter((section) => section.results.length > 0),
[channelResults, messageResults, searchQuery.data?.found, userResults],
);
const results = React.useMemo<SearchResult[]>(
() => sections.flatMap((section) => section.results),
[sections],
);
const resultProfilesQuery = useUsersBatchQuery(
messageResults.map((hit) => hit.pubkey),
{
enabled: shouldSearch && messageResults.length > 0,
},
);
const resultProfiles = resultProfilesQuery.data?.profiles;
React.useEffect(() => {
setSelectedIndex((current) => {
if (results.length === 0) {
return 0;
}
return Math.min(current, results.length - 1);
});
}, [results]);
const reset = React.useCallback(() => {
setQuery("");
setSelectedIndex(0);
setIsFocused(false);
inputRef.current?.blur();
}, []);
const openResult = React.useCallback(
(result: SearchResult) => {
reset();
if (result.kind === "channel") {
onOpenChannel(result.channel.id);
return;
}
if (result.kind === "user") {
onOpenUser(result.user.pubkey);
return;
}
onOpenResult(result.hit);
},
[onOpenChannel, onOpenResult, onOpenUser, reset],
);
const selectedResult = results[selectedIndex];
const isSearching = searchQuery.isLoading || usersQuery.isLoading;
const showResults = shouldSearch;
return (
<div className="relative">
<div className="flex h-7 items-center gap-2 rounded-lg border border-border/70 bg-background/75 px-2.5 shadow-sm backdrop-blur">
<Search className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<Input
aria-label="Search Sprout"
className="h-auto border-0 bg-transparent px-0 py-0 text-xs shadow-none placeholder:text-muted-foreground/60 focus-visible:ring-0"
data-testid="global-search"
onBlur={() => {
window.setTimeout(() => setIsFocused(false), 120);
}}
onChange={(event) => {
setQuery(event.target.value);
setSelectedIndex(0);
setIsFocused(true);
}}
onFocus={() => setIsFocused(true)}
onKeyDown={(event) => {
if (event.key === "Escape") {
event.preventDefault();
reset();
return;
}
if (event.key === "ArrowDown" && results.length > 0) {
event.preventDefault();
setSelectedIndex((current) =>
Math.min(current + 1, results.length - 1),
);
return;
}
if (event.key === "ArrowUp" && results.length > 0) {
event.preventDefault();
setSelectedIndex((current) => Math.max(current - 1, 0));
return;
}
if (
event.key === "Enter" &&
!event.nativeEvent.isComposing &&
selectedResult
) {
event.preventDefault();
openResult(selectedResult);
}
}}
placeholder="Search Sprout"
ref={inputRef}
value={query}
/>
</div>
{showResults ? (
<div
className="absolute left-1/2 top-full mt-1 max-h-[60vh] w-[min(520px,64vw)] -translate-x-1/2 overflow-y-auto rounded-2xl border border-border/80 bg-popover p-2 text-popover-foreground shadow-2xl"
data-testid="topbar-search-results"
>
{results.length === 0 ? (
<p className="px-2 py-2 text-xs text-muted-foreground">
{isSearching ? "Searching..." : "No matches found."}
</p>
) : (
<div className="space-y-3">
{sections.map((section) => {
let sectionStartIndex = 0;
for (const previousSection of sections) {
if (previousSection === section) {
break;
}
sectionStartIndex += previousSection.results.length;
}
return (
<section key={section.title}>
<div className="mb-1.5 flex items-center justify-between px-1.5 text-[11px] font-medium text-muted-foreground">
<span>{section.title}</span>
<span>{section.count}</span>
</div>
<div className="space-y-1">
{section.results.map((result, index) => {
const absoluteIndex = sectionStartIndex + index;
return (
<SearchResultShell
icon={resultIcon(result, channelLookup)}
isSelected={absoluteIndex === selectedIndex}
key={resultKey(result)}
leading={
result.kind === "user" ? (
<UserResultAvatar user={result.user} />
) : undefined
}
onClick={() => openResult(result)}
onMouseEnter={() => setSelectedIndex(absoluteIndex)}
testId={resultTestId(result)}
>
{result.kind === "channel" ? (
<ChannelResultBody channel={result.channel} />
) : result.kind === "user" ? (
<UserResultBody user={result.user} />
) : (
<MessageResultBody
currentPubkey={currentPubkey}
hit={result.hit}
resultProfiles={resultProfiles}
/>
)}
</SearchResultShell>
);
})}
</div>
</section>
);
})}
</div>
)}
</div>
) : null}
</div>
);
}
+25 -15
View File
@@ -71,17 +71,6 @@ async function openSearchDialogWithShortcut(
.toBe(true);
}
async function openSearchDialogWithButton(
page: import("@playwright/test").Page,
) {
const searchDialog = page.getByTestId("search-dialog");
const openSearchButton = page.getByTestId("global-search");
await expect(openSearchButton).toBeVisible();
await openSearchButton.click();
await expect(searchDialog).toBeVisible();
}
test.beforeEach(async ({ page }) => {
await installMockBridge(page);
});
@@ -249,7 +238,7 @@ test("opens relay-backed search from the sidebar and loads the exact result", as
test("opens channel matches from search", async ({ page }) => {
await page.goto("/");
await openSearchDialogWithButton(page);
await openSearchDialogWithShortcut(page);
await page.getByTestId("search-input").fill("engineering");
const results = page.getByTestId("search-results");
@@ -276,10 +265,31 @@ test("opens channel matches from search", async ({ page }) => {
await expect(page.getByTestId("chat-title")).toHaveText("engineering");
});
test("topbar search keeps focus inline until enter", async ({ page }) => {
await page.goto("/");
const topbarSearch = page.getByTestId("global-search");
await expect(topbarSearch).toBeVisible();
await topbarSearch.fill("engineering");
const inlineResults = page.getByTestId("topbar-search-results");
await expect(page.getByTestId("search-dialog")).not.toBeVisible();
await expect(inlineResults).toContainText("Channels");
await expect(inlineResults).toContainText("engineering");
await topbarSearch.press("Enter");
await expect(page).toHaveURL(
/#\/channels\/1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9$/,
);
await expect(page.getByTestId("chat-title")).toHaveText("engineering");
});
test("opens people matches from global search", async ({ page }) => {
await page.goto("/");
await openSearchDialogWithButton(page);
await openSearchDialogWithShortcut(page);
await page.getByTestId("search-input").fill("alice");
const results = page.getByTestId("search-results");
@@ -303,7 +313,7 @@ test("search results use your resolved profile label instead of You", async ({
}) => {
await page.goto("/");
await openSearchDialogWithButton(page);
await openSearchDialogWithShortcut(page);
await page.getByTestId("search-input").fill("welcome");
const results = page.getByTestId("search-results");
@@ -318,7 +328,7 @@ test("opens accessible unjoined channels from search in read-only mode", async (
}) => {
await page.goto("/");
await openSearchDialogWithButton(page);
await openSearchDialogWithShortcut(page);
await page.getByTestId("search-input").fill("critique");
const results = page.getByTestId("search-results");