mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-24 10:32:45 +00:00
feat(webui): improve sidebar performance
This commit is contained in:
parent
cb7daa77db
commit
d660573b18
@ -565,6 +565,21 @@ function Shell({
|
||||
setSessionSearchOpen(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: globalThis.KeyboardEvent) => {
|
||||
if (event.defaultPrevented) return;
|
||||
const plainCommandK =
|
||||
(event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey;
|
||||
if (!plainCommandK) return;
|
||||
if (event.key.toLowerCase() !== "k") return;
|
||||
event.preventDefault();
|
||||
onOpenSessionSearch();
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [onOpenSessionSearch]);
|
||||
|
||||
const onSelectSearchResult = useCallback(
|
||||
(key: string) => {
|
||||
setSessionSearchOpen(false);
|
||||
@ -776,17 +791,15 @@ function Shell({
|
||||
</Sheet>
|
||||
) : null}
|
||||
|
||||
{showMainSidebar ? (
|
||||
<SessionSearchDialog
|
||||
open={sessionSearchOpen}
|
||||
onOpenChange={setSessionSearchOpen}
|
||||
sessions={sessions}
|
||||
activeKey={activeKey}
|
||||
loading={loading}
|
||||
titleOverrides={sidebarState.title_overrides}
|
||||
onSelect={onSelectSearchResult}
|
||||
/>
|
||||
) : null}
|
||||
<SessionSearchDialog
|
||||
open={sessionSearchOpen}
|
||||
onOpenChange={setSessionSearchOpen}
|
||||
sessions={sessions}
|
||||
activeKey={activeKey}
|
||||
loading={loading}
|
||||
titleOverrides={sidebarState.title_overrides}
|
||||
onSelect={onSelectSearchResult}
|
||||
/>
|
||||
|
||||
<main className="relative flex h-full min-w-0 flex-1 flex-col">
|
||||
<div
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
Archive,
|
||||
ArchiveRestore,
|
||||
@ -19,6 +25,9 @@ import { deriveTitle, relativeTime } from "@/lib/format";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ChatSummary, SidebarDensity, SidebarSortMode } from "@/lib/types";
|
||||
|
||||
const INITIAL_VISIBLE_SESSIONS = 160;
|
||||
const VISIBLE_SESSIONS_INCREMENT = 160;
|
||||
|
||||
interface ChatListProps {
|
||||
sessions: ChatSummary[];
|
||||
activeKey: string | null;
|
||||
@ -42,7 +51,7 @@ interface ChatListProps {
|
||||
emptyLabel?: string;
|
||||
}
|
||||
|
||||
export function ChatList({
|
||||
export const ChatList = memo(function ChatList({
|
||||
sessions,
|
||||
activeKey,
|
||||
onSelect,
|
||||
@ -65,6 +74,52 @@ export function ChatList({
|
||||
emptyLabel,
|
||||
}: ChatListProps) {
|
||||
const { t } = useTranslation();
|
||||
const [visibleLimit, setVisibleLimit] = useState(INITIAL_VISIBLE_SESSIONS);
|
||||
const labels = useMemo(() => ({
|
||||
pinned: t("chat.groups.pinned"),
|
||||
all: t("chat.groups.all"),
|
||||
today: t("chat.groups.today"),
|
||||
yesterday: t("chat.groups.yesterday"),
|
||||
earlier: t("chat.groups.earlier"),
|
||||
archived: t("chat.groups.archived"),
|
||||
fallbackTitle: t("chat.newChat"),
|
||||
}), [t]);
|
||||
const groups = useMemo(
|
||||
() => groupSessions(sessions, labels, {
|
||||
pinnedKeys,
|
||||
archivedKeys,
|
||||
titleOverrides,
|
||||
showArchived,
|
||||
sort,
|
||||
}),
|
||||
[
|
||||
archivedKeys,
|
||||
labels,
|
||||
pinnedKeys,
|
||||
sessions,
|
||||
showArchived,
|
||||
sort,
|
||||
titleOverrides,
|
||||
],
|
||||
);
|
||||
const limitedGroups = useMemo(
|
||||
() => limitGroups(groups, visibleLimit, activeKey),
|
||||
[activeKey, groups, visibleLimit],
|
||||
);
|
||||
const totalSessionCount = useMemo(
|
||||
() => groups.reduce((total, group) => total + group.sessions.length, 0),
|
||||
[groups],
|
||||
);
|
||||
const visibleSessionCount = useMemo(
|
||||
() => limitedGroups.reduce((total, group) => total + group.sessions.length, 0),
|
||||
[limitedGroups],
|
||||
);
|
||||
const hiddenSessionCount = Math.max(0, totalSessionCount - visibleSessionCount);
|
||||
|
||||
useEffect(() => {
|
||||
setVisibleLimit(INITIAL_VISIBLE_SESSIONS);
|
||||
}, [showArchived, sort]);
|
||||
|
||||
if (loading && sessions.length === 0) {
|
||||
return (
|
||||
<div className="px-3 py-6 text-[12px] text-muted-foreground">
|
||||
@ -81,21 +136,6 @@ export function ChatList({
|
||||
);
|
||||
}
|
||||
|
||||
const groups = groupSessions(sessions, {
|
||||
pinned: t("chat.groups.pinned"),
|
||||
all: t("chat.groups.all"),
|
||||
today: t("chat.groups.today"),
|
||||
yesterday: t("chat.groups.yesterday"),
|
||||
earlier: t("chat.groups.earlier"),
|
||||
archived: t("chat.groups.archived"),
|
||||
fallbackTitle: t("chat.newChat"),
|
||||
}, {
|
||||
pinnedKeys,
|
||||
archivedKeys,
|
||||
titleOverrides,
|
||||
showArchived,
|
||||
sort,
|
||||
});
|
||||
const pinned = new Set(pinnedKeys);
|
||||
const archived = new Set(archivedKeys);
|
||||
const running = new Set(runningChatIds);
|
||||
@ -105,7 +145,7 @@ export function ChatList({
|
||||
return (
|
||||
<div className="h-full min-h-0 min-w-0 overflow-x-hidden overflow-y-auto overscroll-contain">
|
||||
<div className="min-w-0 space-y-3 px-2 py-1.5">
|
||||
{groups.map((group) => (
|
||||
{limitedGroups.map((group) => (
|
||||
<section key={group.label} aria-label={group.label}>
|
||||
<div className="px-2 pb-1 text-[12px] font-medium text-muted-foreground/65">
|
||||
{group.label}
|
||||
@ -228,10 +268,25 @@ export function ChatList({
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
{hiddenSessionCount > 0 ? (
|
||||
<div className="px-2 pb-2 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setVisibleLimit((limit) =>
|
||||
Math.min(totalSessionCount, limit + VISIBLE_SESSIONS_INCREMENT),
|
||||
)
|
||||
}
|
||||
className="h-8 w-full rounded-full text-[12px] font-medium text-muted-foreground transition-colors hover:bg-sidebar-accent/65 hover:text-sidebar-foreground"
|
||||
>
|
||||
{t("chat.showMore", { count: hiddenSessionCount })}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function SessionActivityIndicator({
|
||||
state,
|
||||
@ -366,6 +421,45 @@ function groupSessions(
|
||||
return groups;
|
||||
}
|
||||
|
||||
function limitGroups(
|
||||
groups: Array<{ label: string; sessions: ChatSummary[] }>,
|
||||
limit: number,
|
||||
activeKey: string | null,
|
||||
): Array<{ label: string; sessions: ChatSummary[] }> {
|
||||
let remaining = Math.max(0, limit);
|
||||
let activeVisible = !activeKey;
|
||||
const out: Array<{ label: string; sessions: ChatSummary[] }> = [];
|
||||
|
||||
for (const group of groups) {
|
||||
const visible = remaining > 0
|
||||
? group.sessions.slice(0, remaining)
|
||||
: [];
|
||||
remaining -= visible.length;
|
||||
if (activeKey && visible.some((session) => session.key === activeKey)) {
|
||||
activeVisible = true;
|
||||
}
|
||||
if (visible.length > 0) {
|
||||
out.push({ label: group.label, sessions: visible });
|
||||
}
|
||||
}
|
||||
|
||||
if (activeVisible || !activeKey) return out;
|
||||
|
||||
for (const group of groups) {
|
||||
const active = group.sessions.find((session) => session.key === activeKey);
|
||||
if (!active) continue;
|
||||
const existing = out.find((item) => item.label === group.label);
|
||||
if (existing) {
|
||||
existing.sessions = [...existing.sessions, active];
|
||||
} else {
|
||||
out.push({ label: group.label, sessions: [active] });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function sortSessions(
|
||||
sessions: ChatSummary[],
|
||||
sort: SidebarSortMode,
|
||||
|
||||
@ -37,13 +37,16 @@ export function SessionSearchDialog({
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
||||
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
const results = useMemo(() => {
|
||||
const sessionResults = useMemo(() => {
|
||||
if (!open) return [];
|
||||
if (!normalizedQuery) return sessions;
|
||||
const terms = normalizedQuery.split(/\s+/).filter(Boolean);
|
||||
return sessions.filter((session) =>
|
||||
sessionMatchesTerms(session, terms, titleOverrides[session.key]),
|
||||
);
|
||||
}, [normalizedQuery, sessions, titleOverrides]);
|
||||
}, [normalizedQuery, open, sessions, titleOverrides]);
|
||||
const itemCount = sessionResults.length;
|
||||
const shortcutLabel = useMemo(getSearchShortcutLabel, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@ -58,9 +61,9 @@ export function SessionSearchDialog({
|
||||
|
||||
useEffect(() => {
|
||||
setHighlightedIndex((index) =>
|
||||
results.length === 0 ? 0 : Math.min(index, results.length - 1),
|
||||
itemCount === 0 ? 0 : Math.min(index, itemCount - 1),
|
||||
);
|
||||
}, [results.length]);
|
||||
}, [itemCount]);
|
||||
|
||||
const handleSelect = (key: string) => {
|
||||
onOpenChange(false);
|
||||
@ -71,17 +74,19 @@ export function SessionSearchDialog({
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
setHighlightedIndex((index) =>
|
||||
results.length === 0 ? 0 : Math.min(index + 1, results.length - 1),
|
||||
itemCount === 0 ? 0 : (index + 1) % itemCount,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
setHighlightedIndex((index) => Math.max(index - 1, 0));
|
||||
setHighlightedIndex((index) =>
|
||||
itemCount === 0 ? 0 : (index - 1 + itemCount) % itemCount,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
const highlighted = results[highlightedIndex];
|
||||
const highlighted = sessionResults[highlightedIndex];
|
||||
if (!highlighted) return;
|
||||
event.preventDefault();
|
||||
handleSelect(highlighted.key);
|
||||
@ -125,70 +130,75 @@ export function SessionSearchDialog({
|
||||
aria-label={t("sidebar.searchAria")}
|
||||
className="h-full min-w-0 flex-1 bg-transparent text-[15px] font-medium text-foreground outline-none placeholder:text-muted-foreground/75"
|
||||
/>
|
||||
<kbd className="hidden h-6 shrink-0 items-center rounded-md border border-border/70 bg-muted/60 px-2 text-[11px] font-medium text-muted-foreground sm:inline-flex">
|
||||
{shortcutLabel}
|
||||
</kbd>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 overflow-y-auto overscroll-contain p-2">
|
||||
<div className="px-2 pb-1.5 pt-1 text-[12px] font-medium text-muted-foreground/70">
|
||||
{sectionLabel}
|
||||
</div>
|
||||
<section>
|
||||
<div className="px-2 pb-1.5 pt-1 text-[12px] font-medium text-muted-foreground/70">
|
||||
{sectionLabel}
|
||||
</div>
|
||||
|
||||
{loading && sessions.length === 0 ? (
|
||||
<div className="px-3 py-7 text-[13px] text-muted-foreground">
|
||||
{t("chat.loading")}
|
||||
</div>
|
||||
) : results.length === 0 ? (
|
||||
<div className="px-3 py-7 text-[13px] text-muted-foreground">
|
||||
{emptyLabel}
|
||||
</div>
|
||||
) : (
|
||||
<ul className="space-y-1">
|
||||
{results.map((session, index) => {
|
||||
const title = titleOverrides[session.key]?.trim() ||
|
||||
session.title?.trim() ||
|
||||
deriveTitle(session.preview, t("chat.newChat"));
|
||||
const preview = session.preview.trim();
|
||||
const showPreview =
|
||||
preview.length > 0 &&
|
||||
preview.toLowerCase() !== title.trim().toLowerCase();
|
||||
const highlighted = index === highlightedIndex;
|
||||
const active = session.key === activeKey;
|
||||
return (
|
||||
<li key={session.key}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect(session.key)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={cn(
|
||||
"flex min-h-12 w-full min-w-0 rounded-xl px-3 py-2.5 text-left transition-colors",
|
||||
highlighted
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-popover-foreground hover:bg-accent/75 hover:text-accent-foreground",
|
||||
)}
|
||||
>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-[14px] font-medium leading-5">
|
||||
{title}
|
||||
</span>
|
||||
{showPreview ? (
|
||||
<span
|
||||
className={cn(
|
||||
"block truncate text-[12px] leading-4",
|
||||
highlighted
|
||||
? "text-accent-foreground/70"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{preview}
|
||||
{loading && sessions.length === 0 ? (
|
||||
<div className="px-3 py-7 text-[13px] text-muted-foreground">
|
||||
{t("chat.loading")}
|
||||
</div>
|
||||
) : sessionResults.length === 0 ? (
|
||||
<div className="px-3 py-7 text-[13px] text-muted-foreground">
|
||||
{emptyLabel}
|
||||
</div>
|
||||
) : (
|
||||
<ul className="space-y-1">
|
||||
{sessionResults.map((session, index) => {
|
||||
const title = titleOverrides[session.key]?.trim() ||
|
||||
session.title?.trim() ||
|
||||
deriveTitle(session.preview, t("chat.newChat"));
|
||||
const preview = session.preview.trim();
|
||||
const showPreview =
|
||||
preview.length > 0 &&
|
||||
preview.toLowerCase() !== title.trim().toLowerCase();
|
||||
const highlighted = index === highlightedIndex;
|
||||
const active = session.key === activeKey;
|
||||
return (
|
||||
<li key={session.key}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect(session.key)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={cn(
|
||||
"flex min-h-12 w-full min-w-0 rounded-xl px-3 py-2.5 text-left transition-colors",
|
||||
highlighted
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-popover-foreground hover:bg-accent/75 hover:text-accent-foreground",
|
||||
)}
|
||||
>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-[14px] font-medium leading-5">
|
||||
{title}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
{showPreview ? (
|
||||
<span
|
||||
className={cn(
|
||||
"block truncate text-[12px] leading-4",
|
||||
highlighted
|
||||
? "text-accent-foreground/70"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{preview}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@ -211,3 +221,13 @@ function sessionMatchesTerms(
|
||||
|
||||
return terms.every((term) => haystack.includes(term));
|
||||
}
|
||||
|
||||
function getSearchShortcutLabel() {
|
||||
if (typeof navigator === "undefined") return "Ctrl K";
|
||||
const platform = navigator.platform.toLowerCase();
|
||||
const apple =
|
||||
platform.includes("mac") ||
|
||||
platform.includes("iphone") ||
|
||||
platform.includes("ipad");
|
||||
return apple ? "⌘K" : "Ctrl K";
|
||||
}
|
||||
|
||||
@ -271,6 +271,7 @@
|
||||
"fallbackTitle": "Chat {{id}}",
|
||||
"loading": "Loading…",
|
||||
"noSessions": "No sessions yet.",
|
||||
"showMore": "Show {{count}} more",
|
||||
"actions": "Chat actions for {{title}}",
|
||||
"activity": {
|
||||
"running": "Agent running",
|
||||
|
||||
@ -224,6 +224,7 @@
|
||||
"fallbackTitle": "Chat {{id}}",
|
||||
"loading": "Cargando…",
|
||||
"noSessions": "Todavía no hay sesiones.",
|
||||
"showMore": "Mostrar {{count}} más",
|
||||
"actions": "Acciones del chat {{title}}",
|
||||
"activity": {
|
||||
"running": "Agent running",
|
||||
|
||||
@ -224,6 +224,7 @@
|
||||
"fallbackTitle": "Discussion {{id}}",
|
||||
"loading": "Chargement…",
|
||||
"noSessions": "Aucune session pour le moment.",
|
||||
"showMore": "Afficher {{count}} de plus",
|
||||
"actions": "Actions de la discussion {{title}}",
|
||||
"activity": {
|
||||
"running": "Agent running",
|
||||
|
||||
@ -224,6 +224,7 @@
|
||||
"fallbackTitle": "Obrolan {{id}}",
|
||||
"loading": "Memuat…",
|
||||
"noSessions": "Belum ada sesi.",
|
||||
"showMore": "Tampilkan {{count}} lagi",
|
||||
"actions": "Aksi obrolan untuk {{title}}",
|
||||
"activity": {
|
||||
"running": "Agent running",
|
||||
|
||||
@ -224,6 +224,7 @@
|
||||
"fallbackTitle": "チャット {{id}}",
|
||||
"loading": "読み込み中…",
|
||||
"noSessions": "まだセッションがありません。",
|
||||
"showMore": "さらに {{count}} 件表示",
|
||||
"actions": "「{{title}}」のチャット操作",
|
||||
"activity": {
|
||||
"running": "Agent running",
|
||||
|
||||
@ -224,6 +224,7 @@
|
||||
"fallbackTitle": "채팅 {{id}}",
|
||||
"loading": "불러오는 중…",
|
||||
"noSessions": "아직 세션이 없습니다.",
|
||||
"showMore": "{{count}}개 더 보기",
|
||||
"actions": "{{title}} 채팅 작업",
|
||||
"activity": {
|
||||
"running": "Agent running",
|
||||
|
||||
@ -224,6 +224,7 @@
|
||||
"fallbackTitle": "Trò chuyện {{id}}",
|
||||
"loading": "Đang tải…",
|
||||
"noSessions": "Chưa có phiên nào.",
|
||||
"showMore": "Hiển thị thêm {{count}}",
|
||||
"actions": "Tác vụ cho cuộc trò chuyện {{title}}",
|
||||
"activity": {
|
||||
"running": "Agent running",
|
||||
|
||||
@ -259,6 +259,7 @@
|
||||
"fallbackTitle": "对话 {{id}}",
|
||||
"loading": "加载中…",
|
||||
"noSessions": "还没有会话。",
|
||||
"showMore": "再显示 {{count}} 个",
|
||||
"actions": "“{{title}}” 的会话操作",
|
||||
"activity": {
|
||||
"running": "Agent 正在运行",
|
||||
|
||||
@ -224,6 +224,7 @@
|
||||
"fallbackTitle": "對話 {{id}}",
|
||||
"loading": "載入中…",
|
||||
"noSessions": "目前還沒有會話。",
|
||||
"showMore": "再顯示 {{count}} 個",
|
||||
"actions": "「{{title}}」的會話操作",
|
||||
"activity": {
|
||||
"running": "Agent 正在執行",
|
||||
|
||||
@ -992,6 +992,73 @@ describe("App layout", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("opens search from the keyboard shortcut", async () => {
|
||||
mockSessions = [
|
||||
{
|
||||
key: "websocket:chat-a",
|
||||
channel: "websocket",
|
||||
chatId: "chat-a",
|
||||
createdAt: "2026-04-16T10:00:00Z",
|
||||
updatedAt: "2026-04-16T10:00:00Z",
|
||||
preview: "Existing chat",
|
||||
},
|
||||
];
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
||||
fireEvent.keyDown(window, { key: "k", metaKey: true });
|
||||
|
||||
const dialog = await screen.findByRole("dialog", { name: "Search" });
|
||||
expect(within(dialog).queryByText("Global actions")).not.toBeInTheDocument();
|
||||
expect(within(dialog).getByText("Existing chat")).toBeInTheDocument();
|
||||
|
||||
const textbox = within(dialog).getByRole("textbox", { name: "Search" });
|
||||
fireEvent.change(textbox, { target: { value: "missing" } });
|
||||
expect(within(dialog).queryByText("Existing chat")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.change(textbox, { target: { value: "existing" } });
|
||||
expect(within(dialog).getByText("Existing chat")).toBeInTheDocument();
|
||||
|
||||
fireEvent.keyDown(textbox, { key: "Enter" });
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole("dialog", { name: "Search" })).not.toBeInTheDocument(),
|
||||
);
|
||||
expect(createChatSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps large sidebars light while search still covers every chat", async () => {
|
||||
mockSessions = Array.from({ length: 170 }, (_, index) => {
|
||||
const chatId = `chat-${index}`;
|
||||
return {
|
||||
key: `websocket:${chatId}`,
|
||||
channel: "websocket" as const,
|
||||
chatId,
|
||||
createdAt: new Date(Date.UTC(2026, 3, 16, 12, 0 - index)).toISOString(),
|
||||
updatedAt: new Date(Date.UTC(2026, 3, 16, 12, 0 - index)).toISOString(),
|
||||
title: index === 169 ? "Hidden target" : `Bulk chat ${index}`,
|
||||
preview: "",
|
||||
};
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
||||
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
|
||||
await waitFor(() =>
|
||||
expect(within(sidebar).getByRole("button", { name: "Bulk chat 0" })).toBeInTheDocument(),
|
||||
);
|
||||
expect(within(sidebar).queryByText("Hidden target")).not.toBeInTheDocument();
|
||||
expect(within(sidebar).getByRole("button", { name: "Show 10 more" })).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(within(sidebar).getByRole("button", { name: "Search" }));
|
||||
const dialog = await screen.findByRole("dialog", { name: "Search" });
|
||||
fireEvent.change(within(dialog).getByRole("textbox", { name: "Search" }), {
|
||||
target: { value: "hidden" },
|
||||
});
|
||||
expect(within(dialog).getByText("Hidden target")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens a blank start page without creating an empty chat", async () => {
|
||||
mockSessions = [
|
||||
{
|
||||
|
||||
@ -78,6 +78,7 @@ describe("webui i18n", () => {
|
||||
const common = resource.common;
|
||||
expect(common.app.system.restarting).toBeTruthy();
|
||||
expect(common.sidebar.settings).toBeTruthy();
|
||||
expect(common.chat.showMore).toBeTruthy();
|
||||
expect(common.settings.sidebar.title).toBeTruthy();
|
||||
expect(common.settings.backToChat).toBeTruthy();
|
||||
for (const key of SETTINGS_NAV_KEYS) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user