feat(webui): polish chat layout and titles
Align the WebUI sidebar and chat chrome with the updated design, and generate WebUI session titles asynchronously without blocking turns. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
Xubin Ren
co-authored by
Cursor
parent
d8fd4c80bf
commit
790a03ec28
@@ -1,4 +1,4 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ChatSummary } from "@/lib/types";
|
||||
@@ -7,6 +7,7 @@ const connectSpy = vi.fn();
|
||||
const refreshSpy = vi.fn();
|
||||
const createChatSpy = vi.fn().mockResolvedValue("chat-1");
|
||||
const deleteChatSpy = vi.fn();
|
||||
const toggleThemeSpy = vi.fn();
|
||||
let mockSessions: ChatSummary[] = [];
|
||||
|
||||
vi.mock("@/hooks/useSessions", async (importOriginal) => {
|
||||
@@ -34,7 +35,7 @@ vi.mock("@/hooks/useSessions", async (importOriginal) => {
|
||||
vi.mock("@/hooks/useTheme", () => ({
|
||||
useTheme: () => ({
|
||||
theme: "light" as const,
|
||||
toggle: vi.fn(),
|
||||
toggle: toggleThemeSpy,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -74,6 +75,7 @@ describe("App layout", () => {
|
||||
refreshSpy.mockReset();
|
||||
createChatSpy.mockClear();
|
||||
deleteChatSpy.mockReset();
|
||||
toggleThemeSpy.mockReset();
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
@@ -121,8 +123,11 @@ describe("App layout", () => {
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
||||
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole("button", { name: /^First chat$/ })).toBeInTheDocument(),
|
||||
expect(
|
||||
within(sidebar).getByRole("button", { name: /^First chat$/ }),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
fireEvent.pointerDown(screen.getByLabelText("Chat actions for First chat"), {
|
||||
@@ -140,14 +145,24 @@ describe("App layout", () => {
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole("button", { name: /^Second chat$/ }),
|
||||
within(sidebar).getByRole("button", { name: /^Second chat$/ }),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
expect(screen.queryByText('Delete “First chat”?')).not.toBeInTheDocument();
|
||||
expect(document.body.style.pointerEvents).not.toBe("none");
|
||||
}, 15_000);
|
||||
|
||||
it("opens the Cursor-style settings view from the sidebar", async () => {
|
||||
it("opens the Cursor-style settings view from the header", 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",
|
||||
},
|
||||
];
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (input: RequestInfo | URL) => {
|
||||
@@ -180,10 +195,95 @@ describe("App layout", () => {
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
||||
fireEvent.click(screen.getByRole("button", { name: "Settings" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Open settings" }));
|
||||
|
||||
expect(await screen.findByRole("heading", { name: "General" })).toBeInTheDocument();
|
||||
expect(screen.getByText("AI")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("openai/gpt-4o")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("filters sidebar sessions through the lightweight search row", async () => {
|
||||
mockSessions = [
|
||||
{
|
||||
key: "websocket:chat-alpha",
|
||||
channel: "websocket",
|
||||
chatId: "chat-alpha",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
preview: "Project planning notes",
|
||||
},
|
||||
{
|
||||
key: "websocket:chat-beta",
|
||||
channel: "websocket",
|
||||
chatId: "chat-beta",
|
||||
createdAt: "2026-04-15T10:00:00Z",
|
||||
updatedAt: "2026-04-15T10:00:00Z",
|
||||
preview: "Travel ideas",
|
||||
},
|
||||
];
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
||||
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
|
||||
expect(within(sidebar).getByText("Project planning notes")).toBeInTheDocument();
|
||||
expect(within(sidebar).getByText("Travel ideas")).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(screen.getByRole("textbox", { name: "Search chats" }), {
|
||||
target: { value: "travel" },
|
||||
});
|
||||
|
||||
expect(within(sidebar).queryByText("Project planning notes")).not.toBeInTheDocument();
|
||||
expect(within(sidebar).getByText("Travel ideas")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens a blank start page without creating an empty chat", 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",
|
||||
},
|
||||
];
|
||||
|
||||
const matchMedia = vi.fn().mockImplementation((query: string) => ({
|
||||
matches: query.includes("1024px"),
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
}));
|
||||
vi.stubGlobal("matchMedia", matchMedia);
|
||||
|
||||
const { container } = render(<App />);
|
||||
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Toggle theme from header" }));
|
||||
expect(toggleThemeSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Collapse sidebar" }));
|
||||
const desktopAside = container.querySelector("aside.lg\\:block") as HTMLElement;
|
||||
await waitFor(() => expect(desktopAside.style.width).toBe("0px"));
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Start a new chat" })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Toggle sidebar" }));
|
||||
await waitFor(() => expect(desktopAside.style.width).toBe("272px"));
|
||||
|
||||
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
|
||||
fireEvent.click(within(sidebar).getByRole("button", { name: "New chat" }));
|
||||
expect(createChatSpy).not.toHaveBeenCalled();
|
||||
expect(screen.getByText("What can I do for you?")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Start a new chat" })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Toggle theme from header" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Open settings" })).toBeInTheDocument();
|
||||
|
||||
expect(within(sidebar).getByText("Existing chat")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user