import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, fireEvent, render, screen, waitFor, within, } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { SessionManagerPage } from "@/components/sessions/SessionManagerPage"; import { sessionsApi } from "@/lib/api/sessions"; import type { SessionMessage, SessionMeta } from "@/types"; import { setSessionFixtures } from "../msw/state"; const toastSuccessMock = vi.fn(); const toastErrorMock = vi.fn(); vi.mock("sonner", () => ({ toast: { success: (...args: unknown[]) => toastSuccessMock(...args), error: (...args: unknown[]) => toastErrorMock(...args), }, })); vi.mock("@/components/sessions/SessionToc", () => ({ SessionTocSidebar: () => null, SessionTocDialog: () => null, })); vi.mock("@/components/ConfirmDialog", () => ({ ConfirmDialog: ({ isOpen, title, message, confirmText, cancelText, onConfirm, onCancel, }: { isOpen: boolean; title: string; message: string; confirmText: string; cancelText: string; onConfirm: () => void; onCancel: () => void; }) => isOpen ? (
{title}
{message}
) : null, })); const renderPage = () => { const client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); return { client, ...render( , ), }; }; const openSearch = () => { const searchButton = Array.from(screen.getAllByRole("button")).find( (button) => button.querySelector(".lucide-search"), ); if (!searchButton) { throw new Error("Search button not found"); } fireEvent.click(searchButton); }; const closeSearch = () => { const closeButton = Array.from(screen.getAllByRole("button")).find( (button) => button.querySelector(".lucide-x"), ); if (!closeButton) { throw new Error("Search close button not found"); } fireEvent.click(closeButton); }; describe("SessionManagerPage", () => { beforeEach(() => { toastSuccessMock.mockReset(); toastErrorMock.mockReset(); Element.prototype.scrollIntoView = vi.fn(); const sessions: SessionMeta[] = [ { providerId: "codex", sessionId: "codex-session-1", title: "Alpha Session", summary: "Alpha summary", projectDir: "/mock/codex", createdAt: 2, lastActiveAt: 20, sourcePath: "/mock/codex/session-1.jsonl", resumeCommand: "codex resume codex-session-1", }, { providerId: "codex", sessionId: "codex-session-2", title: "Beta Session", summary: "Beta summary", projectDir: "/mock/codex", createdAt: 1, lastActiveAt: 10, sourcePath: "/mock/codex/session-2.jsonl", resumeCommand: "codex resume codex-session-2", }, ]; const messages: Record = { "codex:/mock/codex/session-1.jsonl": [ { role: "user", content: "alpha", ts: 20 }, ], "codex:/mock/codex/session-2.jsonl": [ { role: "user", content: "beta", ts: 10 }, ], }; setSessionFixtures(sessions, messages); }); it("deletes the selected session and selects the next visible session", async () => { renderPage(); await waitFor(() => expect( screen.getByRole("heading", { name: "Alpha Session" }), ).toBeInTheDocument(), ); fireEvent.click(screen.getByRole("button", { name: /删除会话/i })); const dialog = screen.getByTestId("confirm-dialog"); expect(dialog).toBeInTheDocument(); expect(within(dialog).getByText(/Alpha Session/)).toBeInTheDocument(); fireEvent.click(within(dialog).getByRole("button", { name: /删除会话/i })); await waitFor(() => expect( screen.getByRole("heading", { name: "Beta Session" }), ).toBeInTheDocument(), ); expect(screen.queryByText("Alpha Session")).not.toBeInTheDocument(); expect(toastErrorMock).not.toHaveBeenCalled(); expect(toastSuccessMock).toHaveBeenCalled(); }); it("removes a deleted session from filtered search results", async () => { renderPage(); await waitFor(() => expect( screen.getByRole("heading", { name: "Alpha Session" }), ).toBeInTheDocument(), ); openSearch(); fireEvent.change(screen.getByRole("textbox"), { target: { value: "Alpha" }, }); await waitFor(() => expect(screen.getAllByText("Alpha Session")).toHaveLength(2), ); fireEvent.click(screen.getByRole("button", { name: /删除会话/i })); const dialog = screen.getByTestId("confirm-dialog"); fireEvent.click(within(dialog).getByRole("button", { name: /删除会话/i })); await waitFor(() => expect(screen.queryByText("Alpha Session")).not.toBeInTheDocument(), ); expect( screen.getByText("sessionManager.selectSession"), ).toBeInTheDocument(); expect( screen.queryByText("sessionManager.emptySession"), ).not.toBeInTheDocument(); expect(toastErrorMock).not.toHaveBeenCalled(); expect(toastSuccessMock).toHaveBeenCalled(); }); it("restores batch delete controls when deleteMany rejects", async () => { const deleteManySpy = vi .spyOn(sessionsApi, "deleteMany") .mockRejectedValueOnce(new Error("network error")); renderPage(); await waitFor(() => expect( screen.getByRole("heading", { name: "Alpha Session" }), ).toBeInTheDocument(), ); fireEvent.click(screen.getByRole("button", { name: /批量管理/i })); fireEvent.click(screen.getByRole("button", { name: /全选当前/i })); fireEvent.click(screen.getByRole("button", { name: /批量删除/i })); const dialog = screen.getByTestId("confirm-dialog"); fireEvent.click( within(dialog).getByRole("button", { name: /删除所选会话/i }), ); await waitFor(() => expect(toastErrorMock).toHaveBeenCalledWith("network error"), ); await waitFor(() => expect( screen.getByRole("button", { name: /批量删除/i }), ).not.toBeDisabled(), ); deleteManySpy.mockRestore(); }); it("keeps the exit batch mode button visible when search hides all sessions", async () => { renderPage(); await waitFor(() => expect( screen.getByRole("heading", { name: "Alpha Session" }), ).toBeInTheDocument(), ); fireEvent.click(screen.getByRole("button", { name: /批量管理/i })); openSearch(); fireEvent.change(screen.getByRole("textbox"), { target: { value: "NoSuchSession" }, }); await waitFor(() => expect(screen.queryByText("Alpha Session")).toBeNull()); expect(screen.getByRole("button", { name: /退出批量管理/i })).toBeVisible(); }); it("drops hidden selections when search narrows the result set", async () => { renderPage(); await waitFor(() => expect( screen.getByRole("heading", { name: "Alpha Session" }), ).toBeInTheDocument(), ); fireEvent.click(screen.getByRole("button", { name: /批量管理/i })); fireEvent.click(screen.getByRole("button", { name: /全选当前/i })); expect(screen.getByText("已选 2 项")).toBeInTheDocument(); openSearch(); fireEvent.change(screen.getByRole("textbox"), { target: { value: "Alpha" }, }); await waitFor(() => expect(screen.queryByText("Beta Session")).not.toBeInTheDocument(), ); closeSearch(); await waitFor(() => expect(screen.getByText("已选 1 项")).toBeInTheDocument(), ); }); it("removes successfully deleted sessions from the UI before refetch completes", async () => { const view = renderPage(); let resolveInvalidate!: () => void; const invalidateSpy = vi .spyOn(view.client, "invalidateQueries") .mockImplementation( () => new Promise((resolve) => { resolveInvalidate = () => resolve(undefined); }), ); await waitFor(() => expect( screen.getByRole("heading", { name: "Alpha Session" }), ).toBeInTheDocument(), ); fireEvent.click(screen.getByRole("button", { name: /批量管理/i })); fireEvent.click(screen.getByRole("button", { name: /全选当前/i })); fireEvent.click(screen.getByRole("button", { name: /批量删除/i })); const dialog = screen.getByTestId("confirm-dialog"); fireEvent.click( within(dialog).getByRole("button", { name: /删除所选会话/i }), ); await waitFor(() => { expect(screen.queryByText("Alpha Session")).not.toBeInTheDocument(); expect(screen.queryByText("Beta Session")).not.toBeInTheDocument(); }); await act(async () => { resolveInvalidate(); }); invalidateSpy.mockRestore(); }); });