diff --git a/src/frontend/src/components/core/playgroundComponent/chat-view/chat-header/components/__tests__/chat-sidebar.test.tsx b/src/frontend/src/components/core/playgroundComponent/chat-view/chat-header/components/__tests__/chat-sidebar.test.tsx new file mode 100644 index 0000000000..97938766e9 --- /dev/null +++ b/src/frontend/src/components/core/playgroundComponent/chat-view/chat-header/components/__tests__/chat-sidebar.test.tsx @@ -0,0 +1,106 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { ChatSidebar } from "../chat-sidebar"; + +jest.mock( + "@/components/core/playgroundComponent/hooks/use-get-flow-id", + () => ({ + useGetFlowId: () => "flow-1", + }), +); + +type FlowState = { playgroundPage: boolean }; +jest.mock("@/stores/flowStore", () => ({ + __esModule: true, + default: (selector: (state: FlowState) => TResult) => + selector({ playgroundPage: false }), +})); + +type AlertState = { setSuccessData: jest.Mock }; +jest.mock("@/stores/alertStore", () => ({ + __esModule: true, + default: (selector: (state: AlertState) => TResult) => + selector({ setSuccessData: jest.fn() }), +})); + +jest.mock("../session-selector", () => ({ + SessionSelector: ({ + session, + onToggleSelect, + }: { + session: string; + onToggleSelect?: () => void; + }) => ( + + ), +})); + +const baseProps = { + sessions: ["flow-1", "New Session 0", "New Session 1"], + onSessionSelect: jest.fn(), + currentSessionId: "flow-1", + onDeleteSession: jest.fn(), + onRenameSession: jest.fn().mockResolvedValue(undefined), + onBulkDeleteSessions: jest.fn(), +}; + +const checkAll = () => + fireEvent.click(screen.getByTestId("select-all-checkbox")); + +describe("ChatSidebar — Select All row alignment", () => { + it("does not render the bulk-delete button while nothing is selected", () => { + render(); + expect(screen.queryByTestId("bulk-delete-button")).not.toBeInTheDocument(); + }); + + it("renders the bulk-delete button with the same h-8 w-8 p-2 shape as a SessionMoreMenu trigger", () => { + render(); + checkAll(); + const trash = screen.getByTestId("bulk-delete-button"); + expect(trash).toBeInTheDocument(); + expect(trash.className).toContain("h-8"); + expect(trash.className).toContain("w-8"); + expect(trash.className).toContain("p-2"); + // No leftover sm-button padding that would skew horizontal centering. + expect(trash.className).not.toContain("p-0"); + }); + + it("hosts the bulk-delete button in an h-8 row with no extra vertical padding", () => { + render(); + checkAll(); + const trash = screen.getByTestId("bulk-delete-button"); + // Walk up to the Select All row wrapper. Its className must include + // `h-8` and must not add `py-1` / `py-*` padding that would push the + // trash button's center down relative to the SessionSelector rows. + const row = trash.closest("div.flex.items-center.justify-between"); + expect(row).not.toBeNull(); + expect(row!.className).toContain("h-8"); + expect(row!.className).not.toMatch(/\bpy-\d/); + expect(row!.className).not.toMatch(/\bmb-\d/); + }); + + it("calls onBulkDeleteSessions with the checked sessions when clicked", () => { + const onBulkDeleteSessions = jest.fn(); + render( + , + ); + checkAll(); + fireEvent.click(screen.getByTestId("bulk-delete-button")); + expect(onBulkDeleteSessions).toHaveBeenCalledTimes(1); + const [ids] = onBulkDeleteSessions.mock.calls[0]; + expect(ids).toEqual( + expect.arrayContaining(["New Session 0", "New Session 1"]), + ); + // Default session (currentFlowId) is non-selectable and must not be + // included in the bulk delete payload. + expect(ids).not.toContain("flow-1"); + }); +}); diff --git a/src/frontend/src/components/core/playgroundComponent/chat-view/chat-header/components/chat-sidebar.tsx b/src/frontend/src/components/core/playgroundComponent/chat-view/chat-header/components/chat-sidebar.tsx index 7d2ef4a8ce..49c8692973 100644 --- a/src/frontend/src/components/core/playgroundComponent/chat-view/chat-header/components/chat-sidebar.tsx +++ b/src/frontend/src/components/core/playgroundComponent/chat-view/chat-header/components/chat-sidebar.tsx @@ -3,8 +3,8 @@ import { useTranslation } from "react-i18next"; import ForwardedIconComponent from "@/components/common/genericIconComponent"; import ShadTooltip from "@/components/common/shadTooltipComponent"; import { Button } from "@/components/ui/button"; -import useFlowStore from "@/stores/flowStore"; import useAlertStore from "@/stores/alertStore"; +import useFlowStore from "@/stores/flowStore"; import { cn } from "@/utils/utils"; import { useGetFlowId } from "../../../hooks/use-get-flow-id"; import { SessionSelector } from "./session-selector"; @@ -142,18 +142,23 @@ export function ChatSidebar({ ) : (
{sessions.map((session, index) => { - const isDefaultSession = session === currentFlowId; const isFirstNonDefaultSession = index > 0 && sessions[index - 1] === currentFlowId; return (
- {/* Show Select All controls after the default session */} + {/* Show Select All controls after the default session. + Wrapper is sized + padded to mirror a SessionSelector + row (h-8, no extra vertical padding) so the trash + button lines up vertically and horizontally with the + `⋮` MoreMenu triggers in the session rows below. */} {isFirstNonDefaultSession && selectableSessions.length > 0 && ( -
-
+ - - )} -
+ + + + )}
)}